first commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('dashboard renders for authenticated admin', function () {
|
||||
$this->actingAsAdmin()
|
||||
->get('/')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('Dashboard')
|
||||
->has('widgets')
|
||||
);
|
||||
});
|
||||
|
||||
test('dashboard hides user management widgets from roles without the permission', function () {
|
||||
$this->be(UserFactory::user()->create());
|
||||
|
||||
$components = widgetComponents($this->get('/')->assertOk());
|
||||
|
||||
$this->assertNotContains('StatWidget', $components);
|
||||
$this->assertNotContains('RegistrationHistoryWidget', $components);
|
||||
$this->assertNotContains('LatestRegistrationsWidget', $components);
|
||||
$this->assertNotContains('ApprovalNoticeWidget', $components);
|
||||
$this->assertContains('UserActionsWidget', $components);
|
||||
});
|
||||
|
||||
test('dashboard shows user management widgets to roles holding the permission', function () {
|
||||
$this->be(UserFactory::withPermissions('users.manage')->create());
|
||||
|
||||
$components = widgetComponents($this->get('/')->assertOk());
|
||||
|
||||
$this->assertContains('StatWidget', $components);
|
||||
$this->assertContains('RegistrationHistoryWidget', $components);
|
||||
$this->assertContains('LatestRegistrationsWidget', $components);
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
function widgetComponents(TestResponse $response): array
|
||||
{
|
||||
return array_column($response->viewData('page')['props']['widgets'], 'component');
|
||||
}
|
||||
|
||||
test('dashboard redirects guest to login', function () {
|
||||
$this->get('/')->assertRedirect('login');
|
||||
});
|
||||
|
||||
test('dashboard flashes success when email verified session is set', function () {
|
||||
$this->actingAsAdmin()
|
||||
->withSession(['verified' => true])
|
||||
->get('/')
|
||||
->assertOk()
|
||||
->assertSessionHas('success', __('E-Mail verified successfully.'));
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Support\Demo;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
/**
|
||||
* The account UserSeeder creates, which config/demo.php protects by default.
|
||||
*/
|
||||
function demoAccount(): User
|
||||
{
|
||||
return User::where('username', 'admin')->firstOrFail();
|
||||
}
|
||||
|
||||
function enableDemoMode(): void
|
||||
{
|
||||
config(['demo.enabled' => true]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Off by default
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('demo mode is off unless the deployment turns it on', function () {
|
||||
// phpunit.xml pins DEMO_MODE off, because the demo deployment's own .env
|
||||
// turns it on and the suite must not change meaning there. That the
|
||||
// shipped templates default to false is EnvironmentTemplateTest's job.
|
||||
$this->assertFalse(Demo::enabled());
|
||||
});
|
||||
|
||||
test('every write is reachable while demo mode is off', function () {
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('Renamed', Setting::get('app_name'));
|
||||
|
||||
$this->from('/users')
|
||||
->delete('users/'.demoAccount()->id)
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertDatabaseMissing('users', ['username' => 'admin']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Blocked routes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('blocked routes are refused with a flashed error', function (string $url, array $data) {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/')
|
||||
->post($url, $data)
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertSessionHasFlashError('This action is disabled in the demo.');
|
||||
})->with([
|
||||
'general settings' => ['/settings/general', ['app_name' => 'Renamed']],
|
||||
'auth settings' => ['/settings/auth', ['reg_enabled' => false]],
|
||||
'two factor toggle' => ['/settings/auth/2fa/enable', []],
|
||||
'permission matrix' => ['/permissions/save', ['roles' => []]],
|
||||
]);
|
||||
|
||||
test('a blocked settings write leaves the setting alone', function () {
|
||||
Setting::set('app_name', 'Vanguard');
|
||||
Setting::save();
|
||||
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
$this->assertEquals('Vanguard', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('a blocked permission matrix write leaves the role permissions alone', function () {
|
||||
$role = Role::where('name', 'User')->firstOrFail();
|
||||
$before = $role->permissions()->pluck('id')->sort()->values()->all();
|
||||
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/permissions')
|
||||
->post('/permissions/save', ['roles' => [$role->id => []]]);
|
||||
|
||||
$this->assertSessionHasFlashError('This action is disabled in the demo.');
|
||||
$this->assertEquals($before, $role->fresh()->permissions()->pluck('id')->sort()->values()->all());
|
||||
});
|
||||
|
||||
test('a refused Inertia delete answers 303 so the browser does not repeat the verb', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->withHeader('X-Inertia', 'true')
|
||||
->delete('users/'.demoAccount()->id)
|
||||
->assertStatus(303)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
});
|
||||
|
||||
test('the refusal reaches the shared flash prop the layouts turn into a toast', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
// The flash is read by HandleInertiaRequests on the request the redirect
|
||||
// lands on, which is where the layout's watcher fires.
|
||||
$this->get('/settings')
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->where('flash.error', 'This action is disabled in the demo.')
|
||||
);
|
||||
});
|
||||
|
||||
test('reading a page is never blocked', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAsAdmin()->get('/settings')->assertOk();
|
||||
$this->get('/users/'.demoAccount()->id)->assertOk();
|
||||
$this->get('/permissions')->assertOk();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| The demo stays interactive
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('a visitor can still create and delete an ordinary user', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->from('/users/create')
|
||||
->post('users', [
|
||||
'email' => 'jane@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
'role_id' => Role::where('name', 'User')->value('id'),
|
||||
])
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertSessionHasSuccess('User created successfully.');
|
||||
|
||||
$created = User::where('email', 'jane@example.com')->firstOrFail();
|
||||
|
||||
$this->from('/users')
|
||||
->delete('users/'.$created->id)
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertSessionHasSuccess('User deleted successfully.');
|
||||
$this->assertDatabaseMissing('users', ['email' => 'jane@example.com']);
|
||||
});
|
||||
|
||||
test('the demo account can still create a role', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->from('/roles/create')
|
||||
->post('roles', Role::factory()->raw(['name' => 'Editor']))
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertSessionHasSuccess('Role created successfully.');
|
||||
$this->assertDatabaseHas('roles', ['name' => 'Editor']);
|
||||
});
|
||||
|
||||
test('a visitor can still impersonate another user and stop again', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$target = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->get(route('impersonate', $target->id))
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->user()->is($target));
|
||||
|
||||
$this->get(route('impersonate.leave'));
|
||||
|
||||
$this->assertTrue(auth()->user()->is(demoAccount()));
|
||||
});
|
||||
|
||||
test('impersonating does not lift the demo restrictions', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$target = UserFactory::withPermissions('settings.general')->create();
|
||||
|
||||
$this->actingAs(demoAccount())->get(route('impersonate', $target->id));
|
||||
|
||||
$this->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
$this->assertSessionHasFlashError('This action is disabled in the demo.');
|
||||
});
|
||||
|
||||
test('impersonating the demo account does not hand over its login details', function () {
|
||||
enableDemoMode();
|
||||
|
||||
// Taking over a protected account is allowed; changing it is not, because
|
||||
// the self service rules read the impersonated user rather than the one
|
||||
// who signed in.
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->get(route('impersonate', demoAccount()->id));
|
||||
|
||||
$this->assertTrue(auth()->user()->is(demoAccount()));
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/login-details', [
|
||||
'email' => 'hijacked@example.com',
|
||||
'password' => 'hijacked123',
|
||||
'password_confirmation' => 'hijacked123',
|
||||
]);
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertEquals('admin@example.com', demoAccount()->email);
|
||||
});
|
||||
|
||||
test('logging in still works', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$user = UserFactory::withCredentials('jane', 'secret123')->user()->create();
|
||||
|
||||
$this->post('login', ['username' => 'jane', 'password' => 'secret123'])
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertAuthenticatedAs($user);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| The protected account
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('the demo account cannot be deleted', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->delete('users/'.demoAccount()->id)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertDatabaseHas('users', ['username' => 'admin']);
|
||||
});
|
||||
|
||||
test('the demo account cannot have its login details changed by an administrator', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->put('users/'.demoAccount()->id.'/update/login-details', [
|
||||
'email' => 'hijacked@example.com',
|
||||
'password' => 'hijacked123',
|
||||
'password_confirmation' => 'hijacked123',
|
||||
]);
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertEquals('admin@example.com', demoAccount()->email);
|
||||
});
|
||||
|
||||
test('the demo account cannot change its own login details', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$this->actingAs(demoAccount())
|
||||
->from('/profile')
|
||||
->put('profile/login-details', [
|
||||
'email' => 'hijacked@example.com',
|
||||
'password' => 'hijacked123',
|
||||
'password_confirmation' => 'hijacked123',
|
||||
])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertEquals('admin@example.com', demoAccount()->email);
|
||||
});
|
||||
|
||||
test('an ordinary visitor can still change their own login details', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->from('/profile')
|
||||
->put('profile/login-details', [
|
||||
'email' => 'jane@example.com',
|
||||
'password' => 'secret123',
|
||||
'password_confirmation' => 'secret123',
|
||||
])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Login details updated successfully.');
|
||||
$this->assertEquals('jane@example.com', $user->fresh()->email);
|
||||
});
|
||||
|
||||
test('protected accounts can be named by email instead of username', function () {
|
||||
enableDemoMode();
|
||||
config(['demo.protected_users' => ['admin@example.com']]);
|
||||
|
||||
$this->actingAs(UserFactory::admin()->create())
|
||||
->from('/users')
|
||||
->delete('users/'.demoAccount()->id);
|
||||
|
||||
$this->assertSessionHasFlashError('The demo account cannot be modified.');
|
||||
$this->assertDatabaseHas('users', ['username' => 'admin']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Seeded roles and permissions
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('a seeded role cannot be renamed', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$role = Role::where('name', 'Admin')->firstOrFail();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/roles')
|
||||
->put('roles/'.$role->id, Role::factory()->raw(['name' => 'Renamed']))
|
||||
->assertRedirect('/roles');
|
||||
|
||||
$this->assertSessionHasFlashError('Default roles and permissions cannot be modified in the demo.');
|
||||
$this->assertEquals('Admin', $role->fresh()->name);
|
||||
});
|
||||
|
||||
test('a role created during the demo can still be renamed', function () {
|
||||
enableDemoMode();
|
||||
|
||||
$role = Role::factory()->create(['name' => 'Editor']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/roles')
|
||||
->put('roles/'.$role->id, Role::factory()->raw(['name' => 'Renamed']))
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertSessionHasSuccess('Role updated successfully.');
|
||||
$this->assertEquals('Renamed', $role->fresh()->name);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('the configured message replaces every reason', function () {
|
||||
enableDemoMode();
|
||||
config(['demo.message' => 'Grab your own copy to try this.']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'Renamed']);
|
||||
|
||||
$this->assertSessionHasFlashError('Grab your own copy to try this.');
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed --class=RolesSeeder');
|
||||
$this->artisan('db:seed --class=PermissionsSeeder');
|
||||
});
|
||||
|
||||
test('unverified users cannot impersonate other users', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->unverified()->create();
|
||||
|
||||
User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get('/users')->assertRedirect('/email/verify');
|
||||
});
|
||||
|
||||
test('a user with appropriate permission can impersonate other users from a user list page', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get('/users')->assertSee('Impersonate');
|
||||
});
|
||||
|
||||
test('a user dont see impersonate button next to his name in the user list', function () {
|
||||
$user = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($user)->get('/user')->assertDontSee('Impersonate');
|
||||
});
|
||||
|
||||
test('clicking on impersonate button will impersonate the user', function () {
|
||||
$userA = UserFactory::withPermissions('users.manage')->create();
|
||||
$userB = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->get(route('impersonate', $userB))
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->user()->is($userB));
|
||||
});
|
||||
|
||||
test('while impersonating user can stop impersonating by clicking on the header button', function () {
|
||||
$userA = UserFactory::withPermissions('users.manage')->create();
|
||||
$userB = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($userA)->get(route('impersonate', $userB));
|
||||
|
||||
$this->assertTrue(auth()->user()->is($userB));
|
||||
|
||||
$this->get('/')->assertInertia(fn (Assert $page) => $page
|
||||
->where('auth.impersonating', true)
|
||||
);
|
||||
|
||||
$this->get(route('impersonate.leave'));
|
||||
|
||||
$this->assertTrue(auth()->user()->is($userA));
|
||||
});
|
||||
|
||||
test('while impersonating user cannot impersonate other user even if he has a permission', function () {
|
||||
$userA = UserFactory::withPermissions('users.manage')->create();
|
||||
$userB = UserFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$this->actingAs($userA)->get(route('impersonate', $userB));
|
||||
|
||||
$this->get('/user')
|
||||
->assertDontSee('Impersonate');
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
test('install start page is displayed', function () {
|
||||
$this->get('install')
|
||||
->assertOk()
|
||||
->assertViewIs('install.start');
|
||||
});
|
||||
|
||||
test('requirements page is displayed', function () {
|
||||
$this->get('install/requirements')
|
||||
->assertOk()
|
||||
->assertViewIs('install.requirements')
|
||||
->assertViewHas('requirements')
|
||||
->assertViewHas('allLoaded');
|
||||
});
|
||||
|
||||
test('permissions page redirects when requirements not met', function () {
|
||||
// Hard to fake unmet requirements, so just verify the route is reachable.
|
||||
$response = $this->get('install/permissions');
|
||||
|
||||
$this->assertContains(
|
||||
$response->getStatusCode(),
|
||||
[200, 302],
|
||||
'install/permissions should return 200 or redirect to requirements.'
|
||||
);
|
||||
});
|
||||
|
||||
test('database page redirects when prerequisites not met', function () {
|
||||
$response = $this->get('install/database');
|
||||
|
||||
$this->assertContains(
|
||||
$response->getStatusCode(),
|
||||
[200, 302],
|
||||
'install/database should return 200 or redirect.'
|
||||
);
|
||||
});
|
||||
|
||||
test('installation redirects with bad db credentials', function () {
|
||||
$this->post('install/start-installation', [
|
||||
'host' => '127.0.0.1',
|
||||
'username' => 'nope',
|
||||
'password' => 'nope',
|
||||
'database' => 'nope_no_database',
|
||||
'prefix' => '',
|
||||
])->assertRedirect();
|
||||
});
|
||||
|
||||
test('complete page is displayed', function () {
|
||||
$this->get('install/complete')
|
||||
->assertOk()
|
||||
->assertViewIs('install.complete');
|
||||
});
|
||||
|
||||
test('error page is displayed', function () {
|
||||
$this->get('install/error')
|
||||
->assertOk()
|
||||
->assertViewIs('install.error');
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\LoggedIn;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Cache\RateLimiter;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setSettings([
|
||||
'throttle_enabled' => false,
|
||||
]);
|
||||
});
|
||||
|
||||
test('successful login', function () {
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->check());
|
||||
$this->assertTrue($user->is(auth()->user()));
|
||||
});
|
||||
|
||||
test('last login timestamp is updated after successful login', function () {
|
||||
$testDate = Carbon::now();
|
||||
|
||||
Carbon::setTestNow($testDate);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
$this->assertNull($user->last_login);
|
||||
|
||||
loginUser('foo', 'bar');
|
||||
|
||||
$this->assertEquals($testDate->timestamp, $user->fresh()->last_login->timestamp);
|
||||
});
|
||||
|
||||
test('login with wrong credentials will fail', function () {
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertFalse(auth()->check());
|
||||
});
|
||||
|
||||
test('country id remains the same after login', function () {
|
||||
$user = User::factory()->create([
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'country_id' => 688,
|
||||
]);
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertEquals(688, $user->fresh()->country_id);
|
||||
});
|
||||
|
||||
test('throttling', function () {
|
||||
$this->setSettings([
|
||||
'throttle_enabled' => true,
|
||||
'throttle_attempts' => 3,
|
||||
'throttle_lockout_time' => 2, // 2 minutes
|
||||
]);
|
||||
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
loginUser('foo', 'bar');
|
||||
}
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('login')
|
||||
->assertSessionHasErrors('username');
|
||||
|
||||
$this->assertTrue(app(RateLimiter::class)->tooManyAttempts('foo|127.0.0.1', 3));
|
||||
});
|
||||
|
||||
test('login with remember', function () {
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
Setting::set('remember_me', false);
|
||||
|
||||
$this->get('login')
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/Login')
|
||||
->where('settings.remember_me', false)
|
||||
);
|
||||
|
||||
Setting::set('remember_me', true);
|
||||
|
||||
$this->get('login')
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/Login')
|
||||
->where('settings.remember_me', true)
|
||||
);
|
||||
|
||||
loginUser('foo', 'bar', true)
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertNotNull($user->fresh()->remember_token);
|
||||
$this->assertNotNull($user->fresh()->last_login);
|
||||
});
|
||||
|
||||
test('login max number of sessions reached', function () {
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
Setting::set('max_active_sessions', 1);
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => Str::random(),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => fake()->ipv4,
|
||||
'user_agent' => fake()->userAgent,
|
||||
'payload' => 'test',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
]);
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError(trans('auth.max_sessions_reached'));
|
||||
|
||||
$this->assertFalse(Auth::check());
|
||||
});
|
||||
|
||||
test('banned user cannot log in', function () {
|
||||
UserFactory::withCredentials('foo', 'bar')->banned()->create();
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError('Your account is banned by administrator.');
|
||||
});
|
||||
|
||||
test('login with 2fa enabled', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
Event::fake([LoggedIn::class]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => $validCode])
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(auth()->check());
|
||||
|
||||
Event::assertDispatched(LoggedIn::class);
|
||||
});
|
||||
|
||||
test('login with 2fa enabled redirect to custom page', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
Event::fake([LoggedIn::class]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
loginUser('foo', 'bar', redirectTo: 'https://google.com')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => $validCode])
|
||||
->assertRedirect('https://google.com');
|
||||
|
||||
$this->assertTrue(auth()->check());
|
||||
|
||||
Event::assertDispatched(LoggedIn::class);
|
||||
});
|
||||
|
||||
test('login with wrong 2fa token', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => '123123'])->assertRedirect('login');
|
||||
|
||||
$this->assertSessionHasError('Invalid 2FA token.');
|
||||
});
|
||||
|
||||
test('login with wrong 2fa token when custom redirect page is provided', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true, 'throttle_enabled' => false]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
loginUser('foo', 'bar', redirectTo: 'https://google.com')
|
||||
->assertRedirect('auth/two-factor-authentication')
|
||||
->assertSessionHas('auth.2fa.id', $user->id);
|
||||
|
||||
$this->post('auth/two-factor-authentication', ['code' => '123'])
|
||||
->assertRedirect('login?to=https://google.com');
|
||||
|
||||
$this->assertSessionHasError('Invalid 2FA token.');
|
||||
});
|
||||
|
||||
test('force password change on login', function () {
|
||||
$this->setSettings(['password-change.enabled' => true]);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'force_password_change' => true,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'force_password_change' => true,
|
||||
]);
|
||||
|
||||
loginUser('foo', 'bar')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->get('/')->assertRedirect('/profile');
|
||||
|
||||
$this->from('/profile')
|
||||
->put('/profile/login-details', [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'force_password_change' => false,
|
||||
]);
|
||||
});
|
||||
|
||||
function loginUser($username, $password, $remember = false, $redirectTo = null): TestResponse
|
||||
{
|
||||
$url = 'login';
|
||||
|
||||
if ($redirectTo) {
|
||||
$url .= '?to='.urlencode($redirectTo);
|
||||
}
|
||||
|
||||
return test()->post($url, [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'remember' => $remember,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
use App\Mail\ResetPassword;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->setSettings(['forgot_password' => true]);
|
||||
});
|
||||
|
||||
test('password request form is displayed', function () {
|
||||
$this->get('password/reset')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page->component('auth/ForgotPassword'));
|
||||
});
|
||||
|
||||
test('reset link is sent for existing email', function () {
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => $user->email])
|
||||
->assertRedirect('password/reset');
|
||||
|
||||
Mail::assertQueued(ResetPassword::class, fn ($mail) => $mail->hasTo($user->email));
|
||||
});
|
||||
|
||||
test('reset link request with unknown email does not leak existence', function () {
|
||||
Mail::fake();
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => 'nobody@example.com'])
|
||||
->assertRedirect('password/reset')
|
||||
->assertSessionHasNoErrors()
|
||||
->assertSessionHas('success', trans('passwords.sent'));
|
||||
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
|
||||
test('unknown email flashes a string the toast can render', function () {
|
||||
Mail::fake();
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => 'nobody@example.com'])
|
||||
->assertSessionHas('success', fn ($success) => is_string($success));
|
||||
|
||||
$this->get('password/reset')
|
||||
->assertInertia(fn (Assert $page) => $page->where('flash.success', trans('passwords.sent')));
|
||||
});
|
||||
|
||||
test('a throttled reset link request reports the failure against the email field', function () {
|
||||
Mail::fake();
|
||||
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
|
||||
$this->post('password/email', ['email' => $user->email]);
|
||||
|
||||
$this->from('password/reset')
|
||||
->post('password/email', ['email' => $user->email])
|
||||
->assertRedirect('password/reset')
|
||||
->assertSessionHasErrors(['email' => trans('passwords.throttled')]);
|
||||
});
|
||||
|
||||
test('reset form is displayed with token', function () {
|
||||
$this->get('password/reset/sometoken?email=foo@bar.com')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('auth/ResetPassword')
|
||||
->where('token', 'sometoken')
|
||||
->where('email', 'foo@bar.com')
|
||||
);
|
||||
});
|
||||
|
||||
test('password can be reset with valid token', function () {
|
||||
Event::fake([PasswordReset::class]);
|
||||
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
$token = Password::broker()->createToken($user);
|
||||
|
||||
$this->post('password/reset', [
|
||||
'email' => $user->email,
|
||||
'token' => $token,
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])->assertRedirect('/');
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->fresh()->password));
|
||||
$this->assertTrue(auth()->check());
|
||||
$this->assertTrue($user->is(auth()->user()));
|
||||
|
||||
Event::assertDispatched(PasswordReset::class);
|
||||
});
|
||||
|
||||
test('password cannot be reset with invalid token', function () {
|
||||
$user = User::factory()->create(['email' => 'foo@bar.com']);
|
||||
$originalHash = $user->password;
|
||||
|
||||
$this->from('password/reset/invalid-token')
|
||||
->post('password/reset', [
|
||||
'email' => $user->email,
|
||||
'token' => 'invalid-token',
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'new-password',
|
||||
])
|
||||
->assertRedirect('password/reset/invalid-token');
|
||||
|
||||
$this->assertSame($originalHash, $user->fresh()->password);
|
||||
$this->assertFalse(auth()->check());
|
||||
});
|
||||
|
||||
test('password reset requires password confirmation', function () {
|
||||
$user = User::factory()->create();
|
||||
$token = Password::broker()->createToken($user);
|
||||
|
||||
$this->from('password/reset/'.$token)
|
||||
->post('password/reset', [
|
||||
'email' => $user->email,
|
||||
'token' => $token,
|
||||
'password' => 'new-password',
|
||||
'password_confirmation' => 'different',
|
||||
])
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('password reset routes unavailable when disabled', function () {
|
||||
$this->setSettings(['forgot_password' => false]);
|
||||
|
||||
$this->get('password/reset')->assertStatus(404);
|
||||
$this->post('password/email', ['email' => 'foo@bar.com'])->assertStatus(404);
|
||||
});
|
||||
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
use App\Events\Permission\Created;
|
||||
use App\Events\Permission\Deleted;
|
||||
use App\Events\Permission\Updated;
|
||||
use App\Events\Role\PermissionsUpdated;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Role;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
|
||||
$this->be(UserFactory::admin()->create());
|
||||
});
|
||||
|
||||
test('permissions list', function () {
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->get('permissions')
|
||||
->assertSee($permission->display_name);
|
||||
});
|
||||
|
||||
test('only users with appropriate permissions can access the permissions list page', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get('/permissions')->assertStatus(403);
|
||||
$this->actingAs($userB)->get('/permissions')->assertOk();
|
||||
});
|
||||
|
||||
test('permission list with roles', function () {
|
||||
$permission = Permission::factory()->create();
|
||||
$role = Role::first();
|
||||
|
||||
$role->permissions()->attach($permission->id);
|
||||
|
||||
$this->get('permissions')
|
||||
->assertInertia(function (Assert $page) use ($permission, $role) {
|
||||
$page->component('permissions/Index');
|
||||
|
||||
$props = $page->toArray()['props'];
|
||||
|
||||
$this->assertContains(
|
||||
$permission->display_name,
|
||||
collect($props['permissions'])->pluck('display_name')->all()
|
||||
);
|
||||
|
||||
// The granted matrix replaces the roles[{id}][] checkbox names.
|
||||
$this->assertContains($permission->id, $props['granted'][$role->id]);
|
||||
});
|
||||
});
|
||||
|
||||
test('save role permissions', function () {
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
Event::fake([
|
||||
PermissionsUpdated::class,
|
||||
]);
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$this->post('permissions/save', [
|
||||
'roles' => [
|
||||
$role->id => [$permission->id],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSessionHasSuccess('Permissions saved successfully.');
|
||||
|
||||
$this->assertDatabaseHas('permission_role', [
|
||||
'role_id' => $role->id,
|
||||
'permission_id' => $permission->id,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(PermissionsUpdated::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permissions can save role permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$data = [$role->id => [$permission->id]];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->post('permissions/save', ['roles' => $data])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseMissing('permission_role', [
|
||||
'role_id' => $role->id,
|
||||
'permission_id' => $permission->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->post('permissions/save', ['roles' => $data]);
|
||||
|
||||
$this->assertDatabaseHas('permission_role', [
|
||||
'role_id' => $role->id,
|
||||
'permission_id' => $permission->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('save role permissions if no permission is selected for specific role', function () {
|
||||
Event::fake([
|
||||
PermissionsUpdated::class,
|
||||
]);
|
||||
|
||||
$role1 = Role::factory()->create();
|
||||
$permission1 = Permission::factory()->create();
|
||||
$role1->permissions()->attach($permission1->id);
|
||||
|
||||
$role2 = Role::factory()->create();
|
||||
$permission2 = Permission::factory()->create();
|
||||
$role2->permissions()->attach($permission2->id);
|
||||
|
||||
$this->post('/permissions/save', [
|
||||
'roles' => [
|
||||
$role1->id => [$permission1->id],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertSessionHasSuccess('Permissions saved successfully.');
|
||||
|
||||
$this->assertDatabaseHas('permission_role', [
|
||||
'role_id' => $role1->id,
|
||||
'permission_id' => $permission1->id,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseMissing('permission_role', [
|
||||
'role_id' => $role2->id,
|
||||
'permission_id' => $permission2->id,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(PermissionsUpdated::class);
|
||||
});
|
||||
|
||||
test('create permission', function () {
|
||||
$this->app->instance('middleware.disable', false);
|
||||
|
||||
Event::fake([
|
||||
Created::class,
|
||||
]);
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->post('permissions', $data)
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertSessionHasSuccess('Permission created successfully.');
|
||||
$this->assertDatabaseHas('permissions', $data);
|
||||
|
||||
Event::assertDispatched(Created::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can create new permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->post('permissions', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseMissing('permissions', $data);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->post('permissions', $data)
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertDatabaseHas('permissions', $data);
|
||||
});
|
||||
|
||||
test('permission name must have valid format', function () {
|
||||
$this->app->instance('middleware.disable', false);
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$response = $this->post('permissions', validPermissionParams(['name' => 'invalid name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseMissing('permissions', $data);
|
||||
|
||||
$response = $this->post('permissions', validPermissionParams(['name' => 'invalid*name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertDatabaseMissing('permissions', $data);
|
||||
});
|
||||
|
||||
test('update permission', function () {
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
Event::fake([
|
||||
Updated::class,
|
||||
]);
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->get("permissions/{$permission->id}/edit")
|
||||
->assertOk()
|
||||
->assertSee($permission->id)
|
||||
->assertSee($permission->name)
|
||||
->assertSee($permission->display_name);
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->put("permissions/{$permission->id}", $data);
|
||||
|
||||
$this->assertSessionHasSuccess('Permission updated successfully.');
|
||||
$this->assertDatabaseHas('permissions', $data + ['id' => $permission->id]);
|
||||
|
||||
Event::assertDispatched(Updated::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update existing permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$data = validPermissionParams();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->put("permissions/{$permission->id}", $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals($permission->toArray(), $permission->fresh()->toArray());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->put("permissions/{$permission->id}", $data)
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$permission->refresh();
|
||||
|
||||
$this->assertEquals($permission->name, $data['name']);
|
||||
$this->assertEquals($permission->display_name, $data['display_name']);
|
||||
$this->assertEquals($permission->description, $data['description']);
|
||||
});
|
||||
|
||||
test('removable attribute cannot be changed on update', function () {
|
||||
$permission = Permission::factory()->create(['removable' => false]);
|
||||
|
||||
$data = validPermissionParams(['removable' => true]);
|
||||
|
||||
$this->put("permissions/{$permission->id}", $data);
|
||||
|
||||
$permission->refresh();
|
||||
|
||||
$this->assertEquals($permission->name, $data['name']);
|
||||
$this->assertEquals($permission->display_name, $data['display_name']);
|
||||
$this->assertEquals($permission->description, $data['description']);
|
||||
$this->assertFalse($permission->removable);
|
||||
});
|
||||
|
||||
test('permission name must have valid format while updating the permission', function () {
|
||||
$permission = Permission::factory()->create(['name' => 'foo']);
|
||||
|
||||
$response = $this->put('permissions/'.$permission->id, validPermissionParams(['name' => 'invalid name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertEquals('foo', $permission->fresh()->name);
|
||||
|
||||
$response = $this->put('permissions/'.$permission->id, validPermissionParams(['name' => 'invalid*name']));
|
||||
$response->assertSessionHasErrors('name');
|
||||
$this->assertEquals('foo', $permission->fresh()->name);
|
||||
});
|
||||
|
||||
test('delete permission', function () {
|
||||
Event::fake([
|
||||
Deleted::class,
|
||||
]);
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->delete(route('permissions.destroy', $permission->id))
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertSessionHasSuccess('Permission deleted successfully.');
|
||||
$this->assertDatabaseMissing('permissions', ['id' => $permission->id]);
|
||||
|
||||
Event::assertDispatched(Deleted::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can delete permissions', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('permissions.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$permission = Permission::factory()->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->delete("permissions/{$permission->id}")
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNotNull($permission->fresh());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->delete("permissions/{$permission->id}")
|
||||
->assertRedirect('/permissions');
|
||||
|
||||
$this->assertNull($permission->fresh());
|
||||
});
|
||||
|
||||
test('non removable permissions cannot be removed', function () {
|
||||
$permission = Permission::factory()->create(['removable' => false]);
|
||||
|
||||
$this->delete(route('permissions.destroy', $permission->id))
|
||||
->assertStatus(404);
|
||||
|
||||
$this->assertNotNull($permission->fresh());
|
||||
});
|
||||
|
||||
function validPermissionParams(array $override = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'name' => 'foo_permission',
|
||||
'display_name' => 'Foo Permission',
|
||||
'description' => 'the description',
|
||||
], $override);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('user can see their active sessions', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertProfileSession($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('profile/sessions')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('users/Sessions')
|
||||
->where('adminView', false)
|
||||
->where('user.id', $user->id)
|
||||
->has('sessions', 1)
|
||||
->where('sessions.0.id', $sessionId)
|
||||
);
|
||||
});
|
||||
|
||||
test('user can invalidate their session', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertProfileSession($user);
|
||||
|
||||
$this->actingAs($user)
|
||||
->delete("profile/sessions/{$sessionId}/invalidate")
|
||||
->assertRedirect(route('profile.sessions'));
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['id' => $sessionId]);
|
||||
});
|
||||
|
||||
test('guest cannot access profile sessions', function () {
|
||||
$this->get('profile/sessions')->assertRedirect('login');
|
||||
});
|
||||
|
||||
function insertProfileSession(User $user): string
|
||||
{
|
||||
$id = Str::random();
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => $id,
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => fake()->ipv4,
|
||||
'user_agent' => fake()->userAgent,
|
||||
'payload' => 'foo',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\Approved;
|
||||
use App\Mail\UserRegistered;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('registration form is displayed when registration is enabled', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$this->get('register')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page->component('auth/Register'));
|
||||
});
|
||||
|
||||
test('registration form exposes the captcha sitekey only when captcha is enabled', function () {
|
||||
$this->setSettings(['reg_enabled' => true, 'registration.captcha.enabled' => false]);
|
||||
|
||||
$this->get('register')
|
||||
->assertInertia(fn (Assert $page) => $page->where('captchaSitekey', null));
|
||||
|
||||
$this->setSettings(['registration.captcha.enabled' => true]);
|
||||
config(['captcha.sitekey' => 'test-sitekey']);
|
||||
|
||||
$this->get('register')
|
||||
->assertInertia(fn (Assert $page) => $page->where('captchaSitekey', 'test-sitekey'));
|
||||
});
|
||||
|
||||
test('when registration is disabled a visitor cannot see the registration form', function () {
|
||||
$this->setSettings(['reg_enabled' => false]);
|
||||
|
||||
$this->get('register')->assertStatus(404);
|
||||
|
||||
$this->get('login')->assertDontSee('You don\'t have an account?');
|
||||
});
|
||||
|
||||
test('registration with approval enabled', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'approval.enabled' => true,
|
||||
'reg_email_confirmation' => true,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Notification::fake();
|
||||
Event::fake([
|
||||
Approved::class,
|
||||
]);
|
||||
|
||||
$data = getRegistrationFormStubData();
|
||||
|
||||
$this->post('/register', $data)->assertRedirect('/login');
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation', 'tos']);
|
||||
$expected += ['status' => UserStatus::UNCONFIRMED];
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
$user->email_verified_at = now();
|
||||
$user->status = UserStatus::WAITING_APPROVAL;
|
||||
$user->save();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from("/users/{$user->id}/edit")
|
||||
->put("/users/{$user->id}/update/approve")
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$expected = array_merge($expected, ['status' => UserStatus::ACTIVE]);
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
Event::assertDispatched(Approved::class);
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('registration with email confirmation', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'approval.enabled' => false,
|
||||
'reg_email_confirmation' => true,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$data = getRegistrationFormStubData();
|
||||
|
||||
$this->post('/register', $data)->assertRedirect('/');
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation', 'tos']);
|
||||
$expected += ['status' => UserStatus::UNCONFIRMED];
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('registration without email confirmation', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'reg_email_confirmation' => false,
|
||||
'notifications_signup_email' => false,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Notification::fake();
|
||||
|
||||
$data = getRegistrationFormStubData();
|
||||
|
||||
$this->post('/register', $data)->assertRedirect('/');
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation', 'tos']);
|
||||
$expected += ['status' => UserStatus::ACTIVE];
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
|
||||
Notification::assertNotSentTo(
|
||||
User::where('email', $data['email'])->first(),
|
||||
VerifyEmail::class
|
||||
);
|
||||
});
|
||||
|
||||
test('email notification is being sent when new user registers', function () {
|
||||
$this->setSettings([
|
||||
'app_name' => 'foo',
|
||||
'reg_enabled' => true,
|
||||
'reg_email_confirmation' => false,
|
||||
'notifications_signup_email' => true,
|
||||
'registration.captcha.enabled' => false,
|
||||
'tos' => true,
|
||||
]);
|
||||
|
||||
Mail::fake();
|
||||
|
||||
$admin = UserFactory::admin()->email('john.doe@test.com')->create();
|
||||
$user1 = UserFactory::user()->email('jane.doe@test.com')->create();
|
||||
$user2 = UserFactory::user()->email('josh.doe@test.com')->create();
|
||||
|
||||
$this->post('/register', getRegistrationFormStubData());
|
||||
|
||||
Mail::assertQueued(UserRegistered::class, 2);
|
||||
});
|
||||
|
||||
test('redirect to custom page after login', function () {
|
||||
UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
$this->post('/login', [
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'to' => 'http://www.google.com',
|
||||
])->assertRedirect('http://www.google.com');
|
||||
});
|
||||
|
||||
test('custom redirect page is available after failed login attempt', function () {
|
||||
$to = 'http://www.google.com';
|
||||
|
||||
$this->post('/login', [
|
||||
'username' => 'foo',
|
||||
'password' => 'bar',
|
||||
'to' => 'http://www.google.com',
|
||||
])->assertRedirect('login?to='.$to);
|
||||
});
|
||||
|
||||
test('access to auth pages is not allowed for authenticated users', function () {
|
||||
$this->setSettings([
|
||||
'reg_enabled' => true,
|
||||
'2fa.enabled' => true,
|
||||
'forgot_password' => true,
|
||||
]);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
$this->be($user);
|
||||
|
||||
$forbiddenGetRoutes = [
|
||||
'login', 'register', 'password/reset', 'password/reset/123',
|
||||
'auth/facebook/login', 'auth/facebook/callback',
|
||||
];
|
||||
|
||||
foreach ($forbiddenGetRoutes as $route) {
|
||||
$this->get($route)->assertRedirect('/');
|
||||
}
|
||||
|
||||
$this->get('auth/two-factor-authentication')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
function getRegistrationFormStubData()
|
||||
{
|
||||
return [
|
||||
'email' => 'test@test.com',
|
||||
'username' => 'johndoe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
'tos' => 1,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Role;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('guests cannot view role list', function () {
|
||||
$this->get('/roles')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot view role list', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get('/roles')->assertStatus(403);
|
||||
$this->actingAs($userB)->get('/roles')->assertOk();
|
||||
});
|
||||
|
||||
test('roles list is displayed properly', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get('/roles')
|
||||
->assertInertia(function (Assert $page) use ($roleA, $roleB) {
|
||||
// 2 default roles are created when db is seeded
|
||||
$page->component('roles/Index')->has('roles', 4);
|
||||
|
||||
$names = collect($page->toArray()['props']['roles'])->pluck('name');
|
||||
|
||||
$this->assertTrue($names->contains($roleA->name));
|
||||
$this->assertTrue($names->contains($roleB->name));
|
||||
});
|
||||
});
|
||||
|
||||
test('create role', function () {
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post('/roles', $data)
|
||||
->assertRedirect('/roles');
|
||||
|
||||
$this->assertSessionHasSuccess('Role created successfully.');
|
||||
$this->assertDatabaseHas('roles', $data);
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot create new roles', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->post('/roles', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertDatabaseMissing('roles', $data);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->post('/roles', $data)
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertDatabaseHas('roles', $data);
|
||||
});
|
||||
|
||||
test('update role', function () {
|
||||
$role = Role::factory()->create(['name' => 'foo']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get("roles/{$role->id}/edit")
|
||||
->assertOk()
|
||||
->assertSee($role->name)
|
||||
->assertSee($role->display_name)
|
||||
->assertSee($role->description);
|
||||
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->put("/roles/{$role->id}", $data);
|
||||
|
||||
$this->assertSessionHasSuccess('Role updated successfully.');
|
||||
$this->assertDatabaseHas('roles', $data + ['id' => $role->id]);
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot update role', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$role = Role::factory()->create(['name' => 'foo']);
|
||||
|
||||
$data = Role::factory()->raw();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->put("/roles/{$role->id}", $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals($role->toArray(), $role->fresh()->toArray());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->put("/roles/{$role->id}", $data)
|
||||
->assertRedirect('roles');
|
||||
|
||||
$role->refresh();
|
||||
|
||||
$this->assertEquals($role->name, $data['name']);
|
||||
$this->assertEquals($role->display_name, $data['display_name']);
|
||||
$this->assertEquals($role->description, $data['description']);
|
||||
});
|
||||
|
||||
test('removable attribute cannot be changed on update', function () {
|
||||
$role = RoleFactory::unremovable()->create();
|
||||
|
||||
$data = Role::factory()->raw(['removable' => true]);
|
||||
|
||||
$this->actingAsAdmin()->put("/roles/{$role->id}", $data);
|
||||
|
||||
$role->refresh();
|
||||
|
||||
$this->assertEquals($role->name, $data['name']);
|
||||
$this->assertEquals($role->display_name, $data['display_name']);
|
||||
$this->assertEquals($role->description, $data['description']);
|
||||
$this->assertFalse($role->removable);
|
||||
});
|
||||
|
||||
test('delete role', function () {
|
||||
$role = RoleFactory::removable()->create();
|
||||
|
||||
$this->actingAsAdmin()->delete(route('roles.destroy', $role));
|
||||
|
||||
$this->assertNull($role->fresh());
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot delete role', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('roles.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$role = Role::factory()->create(['name' => 'foo']);
|
||||
|
||||
$this->actingAs($userA)
|
||||
->delete(route('roles.destroy', $role->id))
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNotNull($role->fresh());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->delete(route('roles.destroy', $role->id))
|
||||
->assertRedirect('roles');
|
||||
|
||||
$this->assertNull($role->fresh());
|
||||
});
|
||||
|
||||
test('users receive default role after their role is deleted', function () {
|
||||
$user = UserFactory::create();
|
||||
$role = Role::factory()->create(['removable' => true]);
|
||||
$userRole = Role::where('name', 'User')->first();
|
||||
|
||||
$user->setRole($role);
|
||||
|
||||
$this->assertTrue($user->fresh()->hasRole($role->name));
|
||||
|
||||
$this->actingAsAdmin()->delete(route('roles.destroy', $role));
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'role_id' => $userRole->id,
|
||||
'id' => $user->id,
|
||||
]);
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
$this->assertFalse($user->hasRole($role->name));
|
||||
$this->assertTrue($user->hasRole($userRole->name));
|
||||
});
|
||||
|
||||
test('only removable roles can be deleted', function () {
|
||||
$removableRole = Role::factory()->create(['removable' => true]);
|
||||
$nonRemovableRole = Role::factory()->create(['removable' => false]);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$this->delete(route('roles.destroy', $removableRole->id));
|
||||
|
||||
$this->assertNull($removableRole->fresh());
|
||||
|
||||
$this->delete(route('roles.destroy', $nonRemovableRole->id))
|
||||
->assertStatus(404);
|
||||
|
||||
$this->assertNotNull($nonRemovableRole->fresh());
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('enable two factor', function () {
|
||||
Setting::set('approval.enabled', false);
|
||||
|
||||
$this->assertFalse(Setting::get('approval.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/approval/enable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertTrue(Setting::get('approval.enabled'));
|
||||
});
|
||||
|
||||
test('disable two factor', function () {
|
||||
Setting::set('approval.enabled', true);
|
||||
|
||||
$this->assertTrue(Setting::get('approval.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/approval/disable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertFalse(Setting::get('approval.enabled'));
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('auth settings page can be rendered', function () {
|
||||
$this->actingAsAdmin()
|
||||
->get('/settings/auth')
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('update auth settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$data = authSettingsData();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
assertAuthSettingsWereUpdated($data);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update auth settings', function () {
|
||||
Setting::set('app_name', 'foo');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.auth')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = authSettingsData();
|
||||
|
||||
$data['app_name'] = 'bar';
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNotEquals('bar', (string) Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertEquals('bar', (string) Setting::get('app_name'));
|
||||
});
|
||||
|
||||
function assertAuthSettingsWereUpdated(array $data)
|
||||
{
|
||||
test()->assertEquals((string) $data['remember_me'], (string) Setting::get('remember_me'));
|
||||
test()->assertEquals((string) $data['forgot_password'], (string) Setting::get('forgot_password'));
|
||||
test()->assertEquals((string) $data['login_reset_token_lifetime'], (string) Setting::get('login_reset_token_lifetime'));
|
||||
test()->assertEquals((string) $data['throttle_enabled'], (string) Setting::get('throttle_enabled'));
|
||||
test()->assertEquals((string) $data['throttle_attempts'], (string) Setting::get('throttle_attempts'));
|
||||
test()->assertEquals((string) $data['throttle_lockout_time'], (string) Setting::get('throttle_lockout_time'));
|
||||
test()->assertEquals((string) $data['reg_enabled'], (string) Setting::get('reg_enabled'));
|
||||
test()->assertEquals((string) $data['tos'], (string) Setting::get('tos'));
|
||||
test()->assertEquals((string) $data['reg_email_confirmation'], (string) Setting::get('reg_email_confirmation'));
|
||||
}
|
||||
|
||||
function authSettingsData(): array
|
||||
{
|
||||
return [
|
||||
'remember_me' => 1,
|
||||
'forgot_password' => 1,
|
||||
'login_reset_token_lifetime' => 123,
|
||||
'throttle_enabled' => 1,
|
||||
'throttle_attempts' => 10,
|
||||
'throttle_lockout_time' => 2,
|
||||
'reg_enabled' => 1,
|
||||
'tos' => 1,
|
||||
'reg_email_confirmation' => 1,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('enable captcha', function () {
|
||||
Setting::set('registration.captcha.enabled', false);
|
||||
|
||||
$this->assertFalse(Setting::get('registration.captcha.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/registration/captcha/enable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertTrue(Setting::get('registration.captcha.enabled'));
|
||||
});
|
||||
|
||||
test('disable two factor', function () {
|
||||
Setting::set('registration.captcha.enabled', true);
|
||||
|
||||
$this->assertTrue(Setting::get('registration.captcha.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/registration/captcha/disable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertFalse(Setting::get('registration.captcha.enabled'));
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.general')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
use App\Events\Settings\Updated as SettingsUpdated;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('notifications settings page is displayed', function () {
|
||||
Setting::set('notifications_signup_email', 1);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get('/settings/notifications')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('settings/Notifications')
|
||||
->where('values.notifications_signup_email', 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('update notifications settings', function () {
|
||||
Setting::set('notifications_signup_email', 0);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertRedirect('/settings/notifications');
|
||||
|
||||
$this->assertEquals(1, Setting::get('notifications_signup_email'));
|
||||
});
|
||||
|
||||
test('updating notifications settings dispatches the settings updated event', function () {
|
||||
Event::fake([SettingsUpdated::class]);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertRedirect('/settings/notifications');
|
||||
|
||||
Event::assertDispatched(SettingsUpdated::class);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can view notifications settings', function () {
|
||||
$userA = UserFactory::role(RoleFactory::create())->create();
|
||||
$userB = UserFactory::role(RoleFactory::withPermissions('settings.notifications')->create())->create();
|
||||
|
||||
$this->actingAs($userA)->get('/settings/notifications')->assertStatus(403);
|
||||
|
||||
$this->actingAs($userB)->get('/settings/notifications')->assertOk();
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update notifications settings', function () {
|
||||
Setting::set('notifications_signup_email', 0);
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.notifications')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals(0, Setting::get('notifications_signup_email'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/notifications')
|
||||
->post('/settings/notifications', ['notifications_signup_email' => 1])
|
||||
->assertRedirect('/settings/notifications');
|
||||
|
||||
$this->assertEquals(1, Setting::get('notifications_signup_email'));
|
||||
});
|
||||
|
||||
test('notifications settings are unavailable to guests', function () {
|
||||
$this->get('/settings/notifications')->assertRedirect('login');
|
||||
$this->post('/settings/notifications', ['notifications_signup_email' => 1])->assertRedirect('login');
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('enable two factor', function () {
|
||||
Setting::set('2fa.enabled', false);
|
||||
|
||||
$this->assertFalse(Setting::get('2fa.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/2fa/enable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertTrue(Setting::get('2fa.enabled'));
|
||||
});
|
||||
|
||||
test('disable two factor', function () {
|
||||
Setting::set('2fa.enabled', true);
|
||||
|
||||
$this->assertTrue(Setting::get('2fa.enabled'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth/2fa/disable')
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertFalse(Setting::get('2fa.enabled'));
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update general settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.general')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/general')
|
||||
->post('/settings/general', ['app_name' => 'foo'])
|
||||
->assertRedirect('/settings/general');
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
test('update auth settings', function () {
|
||||
Setting::set('app_name', 'bar');
|
||||
|
||||
$data = settingsPageAuthData();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
assertSettingsPageAuthUpdated($data);
|
||||
});
|
||||
|
||||
test('only users with appropriate permission can update auth settings', function () {
|
||||
Setting::set('app_name', 'foo');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('settings.auth')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = ['app_name' => 'bar'];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertEquals('foo', Setting::get('app_name'));
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from('/settings/auth')
|
||||
->post('/settings/auth', $data)
|
||||
->assertRedirect('/settings/auth');
|
||||
|
||||
$this->assertEquals('bar', Setting::get('app_name'));
|
||||
});
|
||||
|
||||
function assertSettingsPageAuthUpdated(array $data)
|
||||
{
|
||||
test()->assertEquals($data['remember_me'], Setting::get('remember_me'));
|
||||
test()->assertEquals($data['forgot_password'], Setting::get('forgot_password'));
|
||||
test()->assertEquals($data['login_reset_token_lifetime'], Setting::get('login_reset_token_lifetime'));
|
||||
test()->assertEquals($data['throttle_enabled'], Setting::get('throttle_enabled'));
|
||||
test()->assertEquals($data['throttle_attempts'], Setting::get('throttle_attempts'));
|
||||
test()->assertEquals($data['throttle_lockout_time'], Setting::get('throttle_lockout_time'));
|
||||
}
|
||||
|
||||
function settingsPageAuthData(): array
|
||||
{
|
||||
return [
|
||||
'remember_me' => 1,
|
||||
'forgot_password' => 1,
|
||||
'login_reset_token_lifetime' => 123,
|
||||
'throttle_enabled' => 1,
|
||||
'throttle_attempts' => 10,
|
||||
'throttle_lockout_time' => 2,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Socialite\Contracts\Provider as SocialiteProvider;
|
||||
use Laravel\Socialite\Contracts\User as SocialUserContract;
|
||||
use Mockery as m;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('social login for new user', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'username' => null,
|
||||
'email' => $socialUser->getEmail(),
|
||||
'first_name' => 'John',
|
||||
'last_name' => 'Doe',
|
||||
'status' => UserStatus::ACTIVE,
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$user = User::where('email', $socialUser->getEmail())->first();
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
]);
|
||||
|
||||
$this->assertEquals($user->id, Auth::id());
|
||||
});
|
||||
|
||||
test('social login for new user if registration is disabled', function () {
|
||||
$this->setSettings(['reg_enabled' => false]);
|
||||
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError('Only users who already created an account can log in.');
|
||||
});
|
||||
|
||||
test('social login for banned user', function () {
|
||||
$user = UserFactory::banned()->create();
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
DB::table('social_logins')->insert([
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/login');
|
||||
|
||||
$this->assertSessionHasError('Your account is banned by administrator.');
|
||||
});
|
||||
|
||||
test('social login for existing user', function () {
|
||||
$user = User::factory()->create();
|
||||
$socialUser = new StubSocialUser;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
DB::table('social_logins')->insert([
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertEquals($user->id, Auth::id());
|
||||
});
|
||||
|
||||
test('missing email login', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new StubSocialUserWithoutEmail;
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('login');
|
||||
|
||||
$this->assertSessionHasError('You have to provide your email address.');
|
||||
});
|
||||
|
||||
test('social login for user with one word name', function () {
|
||||
$this->setSettings(['reg_enabled' => true]);
|
||||
|
||||
$socialUser = new StubSocialUserWithOneWordName;
|
||||
|
||||
$driver = m::mock(SocialiteProvider::class);
|
||||
$driver->shouldReceive('user')->andReturn($socialUser);
|
||||
|
||||
Socialite::shouldReceive('driver')->with('foo')->andReturn($driver);
|
||||
|
||||
$this->get('auth/foo/callback')
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'username' => null,
|
||||
'email' => $socialUser->getEmail(),
|
||||
'first_name' => 'John',
|
||||
'last_name' => '',
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
|
||||
$user = User::where('email', $socialUser->getEmail())->first();
|
||||
|
||||
$this->assertDatabaseHas('social_logins', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'foo',
|
||||
'provider_id' => $socialUser->getId(),
|
||||
'avatar' => $socialUser->getAvatar(),
|
||||
]);
|
||||
|
||||
$this->assertEquals($user->id, Auth::id());
|
||||
});
|
||||
|
||||
class StubSocialUser implements SocialUserContract
|
||||
{
|
||||
public function getId()
|
||||
{
|
||||
return '123';
|
||||
}
|
||||
|
||||
public function getNickname()
|
||||
{
|
||||
return 'johndoe';
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return 'John Doe';
|
||||
}
|
||||
|
||||
public function getEmail()
|
||||
{
|
||||
return 'john@doe.com';
|
||||
}
|
||||
|
||||
public function getAvatar()
|
||||
{
|
||||
return 'http://www.gravatar.com/avatar';
|
||||
}
|
||||
}
|
||||
|
||||
class StubSocialUserWithoutEmail extends StubSocialUser
|
||||
{
|
||||
public $email = null;
|
||||
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
}
|
||||
|
||||
class StubSocialUserWithOneWordName extends StubSocialUser
|
||||
{
|
||||
public function getName()
|
||||
{
|
||||
return 'John';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\TwoFactorDisabled;
|
||||
use App\Events\User\TwoFactorEnabled;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('the 2fa form is visible on profile page if 2fa is enabled', function () {
|
||||
config(['services.authy.key' => 'test']);
|
||||
|
||||
$this->setSettings(['2fa.enabled' => false]);
|
||||
|
||||
// The tab is rendered client side from the shared setting, so that flag
|
||||
// is what decides whether it appears rather than the markup.
|
||||
$this->assertFalse(twoFactorAuthSetting('profile'));
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->assertTrue(twoFactorAuthSetting('profile'));
|
||||
});
|
||||
|
||||
test('the 2fa form is visible on edit user page if 2fa is enabled', function () {
|
||||
config(['services.authy.key' => 'test']);
|
||||
|
||||
$this->setSettings(['2fa.enabled' => false]);
|
||||
|
||||
$user = UserFactory::create();
|
||||
|
||||
$this->assertFalse(twoFactorAuthSetting("/users/{$user->id}/edit"));
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->assertTrue(twoFactorAuthSetting("/users/{$user->id}/edit"));
|
||||
});
|
||||
|
||||
/**
|
||||
* The `2fa.enabled` shared prop, which drives the Two-Factor tab.
|
||||
*/
|
||||
function twoFactorAuthSetting(string $url): bool
|
||||
{
|
||||
return (bool) test()->actingAsAdmin()
|
||||
->get($url)
|
||||
->viewData('page')['props']['settings']['2fa.enabled'];
|
||||
}
|
||||
|
||||
test('enable 2fa from profile page', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('/two-factor/enable')
|
||||
->assertRedirect('/')
|
||||
->assertSessionHas('tab', '2fa');
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNotNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('enable 2fa from edit user page', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$formData = ['user' => $user->id];
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post("users/{$user->id}/two-factor/enable", $formData)
|
||||
->assertRedirect('/')
|
||||
->assertSessionHas('tab', '2fa');
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNotNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot enable 2fa for other users', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->be(UserFactory::user()->create());
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->post('two-factor/enable', [
|
||||
'user' => $user->id,
|
||||
])->assertStatus(403);
|
||||
|
||||
$this->assertTrue(
|
||||
DB::table('users')
|
||||
->where('id', $user->id)
|
||||
->whereNull('two_factor_secret')
|
||||
->exists()
|
||||
);
|
||||
});
|
||||
|
||||
test('code field is required during 2fa verification', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = '123123';
|
||||
$user->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('two-factor/verify')
|
||||
->assertSessionHasErrors('code');
|
||||
});
|
||||
|
||||
test('the 2fa verification with wrong token will fail', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('two-factor/verify', ['code' => '123123']);
|
||||
|
||||
$this->assertSessionHasError('Invalid 2FA token.');
|
||||
});
|
||||
|
||||
test('successful 2fa code verification', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([
|
||||
TwoFactorEnabled::class,
|
||||
]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->save();
|
||||
|
||||
$validCode = $google2fa->getCurrentOtp(decrypt($user->two_factor_secret));
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('two-factor/verify', ['code' => $validCode])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'two_factor_secret' => $secret,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(TwoFactorEnabled::class);
|
||||
});
|
||||
|
||||
test('user can disable 2fa', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$this->be($user);
|
||||
|
||||
Event::fake([
|
||||
TwoFactorDisabled::class,
|
||||
]);
|
||||
|
||||
$this->from('/profile')
|
||||
->post('two-factor/disable')
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Two-Factor Authentication disabled successfully.');
|
||||
|
||||
Event::assertDispatched(TwoFactorDisabled::class);
|
||||
});
|
||||
|
||||
test('user can disable 2fa for another user', function () {
|
||||
$google2fa = new Google2FA;
|
||||
$secret = encrypt($google2fa->generateSecretKey());
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
Event::fake([
|
||||
TwoFactorDisabled::class,
|
||||
]);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
$user->two_factor_secret = $secret;
|
||||
$user->two_factor_confirmed_at = Carbon::now();
|
||||
$user->save();
|
||||
|
||||
$this->from("/users/{$user->id}/edit")
|
||||
->post("/users/{$user->id}/two-factor/disable", ['user' => $user->id])
|
||||
->assertRedirect("/users/{$user->id}/edit");
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'two_factor_secret' => null,
|
||||
]);
|
||||
|
||||
$this->assertSessionHasSuccess('Two-Factor Authentication disabled successfully.');
|
||||
Event::assertDispatched(TwoFactorDisabled::class);
|
||||
});
|
||||
|
||||
test('user without appropriate permissions cannot disable 2fa for another user', function () {
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->be(UserFactory::user()->create());
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->post('two-factor/disable', ['user' => $user->id])->assertForbidden();
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\ChangedAvatar;
|
||||
use App\Events\User\UpdatedProfileDetails;
|
||||
use App\Models\Role;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->be(UserFactory::create());
|
||||
});
|
||||
|
||||
test('test user can access his profile page', function () {
|
||||
$this->get('/profile')->assertOk();
|
||||
});
|
||||
|
||||
test('test user can update his profile details', function () {
|
||||
Event::fake([
|
||||
UpdatedProfileDetails::class,
|
||||
]);
|
||||
|
||||
$data = getStubDetailsData();
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/details', $data)
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Profile updated successfully.');
|
||||
$this->assertDatabaseHas('users', $data + ['id' => auth()->id()]);
|
||||
|
||||
Event::assertDispatched(UpdatedProfileDetails::class);
|
||||
});
|
||||
|
||||
test('test user cannot change his status while updating the profile', function () {
|
||||
$data = getStubDetailsData();
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/details', $data + ['status' => UserStatus::BANNED->value])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Profile updated successfully.');
|
||||
$this->assertDatabaseHas('users', $data + [
|
||||
'id' => auth()->id(),
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
});
|
||||
|
||||
test('test user cannot change his role while updating the profile', function () {
|
||||
$data = getStubDetailsData();
|
||||
|
||||
$role = Role::factory()->create();
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/details', $data + ['role_id' => $role])
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Profile updated successfully.');
|
||||
|
||||
$this->assertNotEquals($role->id, auth()->user()->role_id);
|
||||
});
|
||||
|
||||
test('test update avatar', function () {
|
||||
Event::fake([
|
||||
ChangedAvatar::class,
|
||||
]);
|
||||
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->image('photo1.jpg', 300, 300),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$this->from('profile')
|
||||
->post('/profile/avatar', $data)
|
||||
->assertRedirect('profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Avatar changed successfully.');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
$this->assertNotNull($user->avatar);
|
||||
Storage::disk('public')->assertExists("upload/users/{$user->avatar}");
|
||||
|
||||
[$width, $height] = getimagesizefromstring(
|
||||
Storage::disk('public')->get("upload/users/{$user->avatar}")
|
||||
);
|
||||
|
||||
$this->assertEquals(160, $width);
|
||||
$this->assertEquals(160, $height);
|
||||
|
||||
Event::assertDispatched(ChangedAvatar::class);
|
||||
});
|
||||
|
||||
test('test update avatar with invalid image file', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->create('foo.txt', 123),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$this->from('profile')
|
||||
->post('/profile/avatar', $data)
|
||||
->assertRedirect('profile')
|
||||
->assertSessionHasErrors('avatar');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
Storage::disk('public')->assertMissing("upload/users/{$user->avatar}");
|
||||
$this->assertNull($user->avatar);
|
||||
});
|
||||
|
||||
test('test update avatar external', function () {
|
||||
Event::fake([
|
||||
ChangedAvatar::class,
|
||||
]);
|
||||
|
||||
$data = ['url' => '//www.gravatar.com/avatar'];
|
||||
$this->post(route('profile.update.avatar-external', auth()->id()), $data)
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertSessionHasSuccess('Avatar changed successfully.');
|
||||
|
||||
$this->assertEquals($data['url'], auth()->user()->fresh()->avatar);
|
||||
|
||||
Event::assertDispatched(ChangedAvatar::class);
|
||||
});
|
||||
|
||||
test('test update user login details', function () {
|
||||
$data = [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => 'milos123123',
|
||||
'password_confirmation' => 'milos123123',
|
||||
];
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/login-details', $data)
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$this->assertSessionHasSuccess('Login details updated successfully.');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
$this->assertEquals($data['email'], $user->email);
|
||||
$this->assertEquals($data['username'], $user->username);
|
||||
$this->assertTrue(Hash::check($data['password'], $user->password));
|
||||
});
|
||||
|
||||
test('test password is not changed if omited on update', function () {
|
||||
auth()->user()->update([
|
||||
'email' => 'john@example.com',
|
||||
'password' => '123123',
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'email' => 'test@test.com',
|
||||
'password' => '',
|
||||
'password_confirmation' => '',
|
||||
];
|
||||
|
||||
$this->from('/profile')
|
||||
->put('profile/login-details', $data)
|
||||
->assertRedirect('/profile');
|
||||
|
||||
$user = auth()->user()->fresh();
|
||||
|
||||
$this->assertEquals($data['email'], $user->email);
|
||||
$this->assertTrue(Hash::check('123123', $user->password));
|
||||
});
|
||||
|
||||
test('test user session invalidation', function () {
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$user = UserFactory::withCredentials('foo', 'bar')->create();
|
||||
|
||||
$this->be($user);
|
||||
|
||||
$agent = $this->app['agent'];
|
||||
$device = $agent->device() ?: 'Unknown';
|
||||
$platform = $agent->platform() ?: 'Unknown';
|
||||
|
||||
// Log-in manually to actually create session record in DB
|
||||
$this->post('/login', ['username' => 'foo', 'password' => 'bar']);
|
||||
|
||||
$this->get('/profile/sessions')
|
||||
->assertSee('127.0.0.1')
|
||||
->assertSee($device)
|
||||
->assertSee($platform)
|
||||
->assertSee($agent->browser());
|
||||
|
||||
$sessionId = DB::table('sessions')->where('user_id', $user->id)->first()->id;
|
||||
$this->delete("profile/sessions/{$sessionId}/invalidate");
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
|
||||
});
|
||||
|
||||
function getStubDetailsData(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => 'foo',
|
||||
'last_name' => 'bar',
|
||||
'birthday' => Carbon::now()->subYears(25)->format('Y-m-d'),
|
||||
'phone' => '12345667',
|
||||
'address' => 'the address',
|
||||
'country_id' => 688, // Serbia,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$this->artisan('db:seed');
|
||||
});
|
||||
|
||||
test('admin can view user sessions', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertUserSession($user);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->get("users/{$user->id}/sessions")
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page
|
||||
->component('users/Sessions')
|
||||
->where('adminView', true)
|
||||
->has('sessions', 1)
|
||||
->where('sessions.0.id', $sessionId)
|
||||
);
|
||||
});
|
||||
|
||||
test('admin can invalidate user session', function () {
|
||||
$user = User::factory()->create();
|
||||
$sessionId = insertUserSession($user);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->delete("users/{$user->id}/sessions/{$sessionId}/invalidate")
|
||||
->assertRedirect(route('user.sessions', $user->id));
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['id' => $sessionId]);
|
||||
});
|
||||
|
||||
test('non admin cannot view user sessions', function () {
|
||||
$admin = User::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get("users/{$admin->id}/sessions")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('guest cannot access user sessions', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->get("users/{$user->id}/sessions")->assertRedirect('login');
|
||||
});
|
||||
|
||||
function insertUserSession(User $user): string
|
||||
{
|
||||
$id = Str::random();
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => $id,
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => fake()->ipv4,
|
||||
'user_agent' => fake()->userAgent,
|
||||
'payload' => 'foo',
|
||||
'last_activity' => Carbon::now()->timestamp,
|
||||
]);
|
||||
|
||||
return $id;
|
||||
}
|
||||
@@ -0,0 +1,644 @@
|
||||
<?php
|
||||
|
||||
use App\Events\User\UpdatedByAdmin;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use App\Support\Enum\UserStatus;
|
||||
use Carbon\Carbon;
|
||||
use Facades\Tests\Setup\RoleFactory;
|
||||
use Facades\Tests\Setup\UserFactory;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->artisan('db:seed', ['--class' => 'RolesSeeder']);
|
||||
$this->artisan('db:seed', ['--class' => 'PermissionsSeeder']);
|
||||
$this->artisan('db:seed', ['--class' => 'CountriesSeeder']);
|
||||
});
|
||||
|
||||
test('guests cannot view the user list page', function () {
|
||||
$this->get('/users')->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('users without appropriate permission cannot view user list page', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get('/users')->assertForbidden();
|
||||
$this->actingAs($userB)->get('/users')->assertOk();
|
||||
});
|
||||
|
||||
test('user collection is being properly passed to the view', function () {
|
||||
$admin = UserFactory::admin()->create();
|
||||
$active = UserFactory::user()->create();
|
||||
$banned = UserFactory::user()->banned()->create();
|
||||
$unconfirmed = UserFactory::user()->unconfirmed()->create();
|
||||
|
||||
$ids = $this->actingAs($admin)->get('users')->viewData('page')['props']['users']['data'];
|
||||
|
||||
$ids = collect($ids)->pluck('id')->all();
|
||||
|
||||
$this->assertCount(4, $ids);
|
||||
$this->assertContains($admin->id, $ids);
|
||||
$this->assertContains($active->id, $ids);
|
||||
$this->assertContains($banned->id, $ids);
|
||||
$this->assertContains($unconfirmed->id, $ids);
|
||||
});
|
||||
|
||||
test('user list is paginated', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$this->get('users')->assertInertia(function (Assert $page) {
|
||||
$page->component('users/Index')->has('users.data', 1);
|
||||
|
||||
$paginator = $page->toArray()['props']['users'];
|
||||
|
||||
$this->assertArrayHasKey('links', $paginator);
|
||||
$this->assertSame(1, $paginator['current_page']);
|
||||
});
|
||||
});
|
||||
|
||||
test('users can be filtered out by search term', function () {
|
||||
User::factory()->create(['first_name' => 'Milos', 'last_name' => 'Stojanovic']);
|
||||
$user2 = User::factory()->create(['first_name' => 'John', 'last_name' => 'Doe']);
|
||||
$user3 = User::factory()->create(['first_name' => 'Jane', 'last_name' => 'Doe']);
|
||||
|
||||
$ids = $this->actingAsAdmin()->get('users?search=doe')->viewData('page')['props']['users']['data'];
|
||||
|
||||
$ids = collect($ids)->pluck('id')->all();
|
||||
|
||||
$this->assertCount(2, $ids);
|
||||
$this->assertContains($user3->id, $ids);
|
||||
$this->assertContains($user2->id, $ids);
|
||||
});
|
||||
|
||||
test('users can be filtered out by status', function () {
|
||||
User::factory()->times(2)->create();
|
||||
User::factory()->times(3)->create(['status' => UserStatus::UNCONFIRMED]);
|
||||
|
||||
$ids = $this->actingAsAdmin()
|
||||
->get('users?status='.UserStatus::BANNED->value)
|
||||
->viewData('page')['props']['users']['data'];
|
||||
|
||||
$ids = collect($ids)->pluck('id')->all();
|
||||
|
||||
$this->assertCount(0, $ids);
|
||||
});
|
||||
|
||||
test('admin can successfully create new users', function () {
|
||||
$data = validUserParams();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasSuccess('User created successfully.');
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
$this->assertDatabaseHas('users', Arr::except($data, ['password', 'password_confirmation']));
|
||||
$this->assertTrue(Hash::check('123123123', $user->password));
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot create new users', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = validUserParams();
|
||||
|
||||
$this->actingAs($userA)->post('/users', $data)->assertForbidden();
|
||||
$this->assertNUll(User::where('email', $data['email'])->first());
|
||||
|
||||
$this->actingAs($userB)->post('/users', $data)->assertRedirect('/users');
|
||||
$this->assertNotNull(User::where('email', $data['email'])->first());
|
||||
});
|
||||
|
||||
test('email field is required while creating a user', function () {
|
||||
$data = Arr::except(validUserParams(), ['email']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('email field must be a valid email while creating a user', function () {
|
||||
$data = validUserParams(['email' => 'foo']);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('email must be unique while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
UserFactory::user()->email('john@example.com')->create();
|
||||
|
||||
$data = validUserParams(['email' => 'john@example.com']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('username is not required', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['username']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertNotNull(User::where('email', $data['email'])->first());
|
||||
});
|
||||
|
||||
test('username must be unique while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
User::factory()->create(['username' => 'johndoe']);
|
||||
|
||||
$data = validUserParams(['username' => 'johndoe']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('username');
|
||||
});
|
||||
|
||||
test('password is required while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['password']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('password must be at least 6 characters long while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['password' => '12345']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('password must be confirmed while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['password_confirmation']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('password');
|
||||
});
|
||||
|
||||
test('birthday must be a valid date while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['birthday' => 'foo']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('birthday');
|
||||
});
|
||||
|
||||
test('role field is required while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams();
|
||||
unset($data['role_id']);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('role_id');
|
||||
});
|
||||
|
||||
test('selected role must exist inside the system while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['role_id' => 123]);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('role_id');
|
||||
});
|
||||
|
||||
test('country id field is not required while creating a user', function () {
|
||||
$data = validUserParams(['country_id' => 0]);
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertSessionHasSuccess('User created successfully.');
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation']);
|
||||
$expected['country_id'] = null;
|
||||
|
||||
$this->assertDatabaseHas('users', $expected);
|
||||
$this->assertTrue(Hash::check('123123123', $user->password));
|
||||
});
|
||||
|
||||
test('country id must exist inside the system while creating a user', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$data = validUserParams(['country_id' => 12345]);
|
||||
|
||||
$this->from('/users')
|
||||
->post('/users', $data)
|
||||
->assertRedirect('/users')
|
||||
->assertSessionHasErrors('country_id');
|
||||
});
|
||||
|
||||
test('admin can view users profile', function () {
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()->get("users/{$user->id}")->assertOk();
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot view profile for another user', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)->get("users/{$userB->id}")->assertForbidden();
|
||||
$this->actingAs($userB)->get("users/{$userA->id}")->assertOk();
|
||||
});
|
||||
|
||||
test('update user details', function () {
|
||||
Event::fake([
|
||||
UpdatedByAdmin::class,
|
||||
]);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$data = validUserParams();
|
||||
|
||||
$this->from("users/{$user->id}/edit")
|
||||
->put("/users/{$user->id}/update/details", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$expected = Arr::except($data, ['password', 'password_confirmation']);
|
||||
|
||||
$this->assertDatabaseHas('users', $expected + ['id' => $user->id]);
|
||||
$this->assertSessionHasSuccess('User updated successfully.');
|
||||
|
||||
Event::assertDispatched(UpdatedByAdmin::class);
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot update other users', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = validUserParams();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from("users/{$userB->id}/edit")
|
||||
->put("/users/{$userB->id}/update/details", $data)
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNotEquals($data['email'], $userB->fresh()->email);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from("users/{$userA->id}/edit")
|
||||
->put("/users/{$userA->id}/update/details", $data)
|
||||
->assertRedirect("users/{$userA->id}/edit");
|
||||
|
||||
$this->assertEquals($data['email'], $userA->fresh()->email);
|
||||
});
|
||||
|
||||
test('banning a user will invalidate all his sessions', function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create(['remember_token' => Str::random(60)]);
|
||||
|
||||
DB::table('sessions')->insert([
|
||||
'id' => Str::random(40),
|
||||
'user_id' => $user->id,
|
||||
'ip_address' => '127.0.0.1',
|
||||
'user_agent' => 'Foo',
|
||||
'payload' => Str::random(),
|
||||
'last_activity' => Carbon::now()->subMinute()->timestamp,
|
||||
]);
|
||||
|
||||
$data = validUserParams(['status' => UserStatus::BANNED->value]);
|
||||
|
||||
$this->put("/users/{$user->id}/update/details", $data);
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
|
||||
$this->assertNull($user->fresh()->remember_token);
|
||||
});
|
||||
|
||||
test('admin can update users login details', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$data = [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
];
|
||||
|
||||
$this->put("users/{$user->id}/update/login-details", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$this->assertSessionHasSuccess('Login details updated successfully.');
|
||||
|
||||
$user = $user->fresh();
|
||||
|
||||
$this->assertEquals($data['email'], $user->email);
|
||||
$this->assertEquals($data['username'], $user->username);
|
||||
$this->assertTrue(Hash::check($data['password'], $user->password));
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot update other users login details', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = [
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'milos',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from("users/{$userB->id}/edit")
|
||||
->put("users/{$userB->id}/update/login-details", $data)
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNotEquals($data['email'], $userB->fresh()->email);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from("users/{$userA->id}/edit")
|
||||
->put("users/{$userA->id}/update/login-details", $data)
|
||||
->assertRedirect("users/{$userA->id}/edit");
|
||||
|
||||
$this->assertEquals($data['email'], $userA->fresh()->email);
|
||||
});
|
||||
|
||||
test('two factor form visibility', function () {
|
||||
config(['services.authy.key' => 'test']);
|
||||
|
||||
$this->setSettings(['2fa.enabled' => false]);
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
// The tab is rendered client side from the shared setting, so that is
|
||||
// what decides whether it appears rather than the markup.
|
||||
$this->assertFalse(usersTwoFactorSetting("users/{$user->id}/edit"));
|
||||
|
||||
$this->setSettings(['2fa.enabled' => true]);
|
||||
|
||||
$this->assertTrue(usersTwoFactorSetting("users/{$user->id}/edit"));
|
||||
});
|
||||
|
||||
function usersTwoFactorSetting(string $url): bool
|
||||
{
|
||||
return (bool) test()->actingAsAdmin()
|
||||
->get($url)
|
||||
->viewData('page')['props']['settings']['2fa.enabled'];
|
||||
}
|
||||
|
||||
test('admin can update avatar on behalf of a user', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->image('photo1.jpg', 300, 300),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from("users/{$user->id}/edit")
|
||||
->post("users/{$user->id}/update/avatar", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$this->assertSessionHasSuccess('Avatar changed successfully.');
|
||||
|
||||
$user->refresh();
|
||||
|
||||
$this->assertNotNull($user->avatar);
|
||||
Storage::disk('public')->assertExists("upload/users/{$user->avatar}");
|
||||
|
||||
[$width, $height] = getimagesizefromstring(
|
||||
Storage::disk('public')->get("upload/users/{$user->avatar}")
|
||||
);
|
||||
|
||||
$this->assertEquals(160, $width);
|
||||
$this->assertEquals(160, $height);
|
||||
});
|
||||
|
||||
test('admin can update avatar on behalf of a user only if valid file is selected', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->create('foo.txt', 123),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->from("users/{$user->id}/edit")
|
||||
->post("users/{$user->id}/update/avatar", $data)
|
||||
->assertRedirect("users/{$user->id}/edit");
|
||||
|
||||
$this->assertNull($user->fresh()->avatar);
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot update avatar for other users', function () {
|
||||
Storage::fake('public');
|
||||
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$data = [
|
||||
'avatar' => UploadedFile::fake()->image('photo1.jpg', 300, 300),
|
||||
'points' => [
|
||||
'x1' => 0,
|
||||
'y1' => 0,
|
||||
'x2' => 200,
|
||||
'y2' => 200,
|
||||
],
|
||||
];
|
||||
|
||||
$this->actingAs($userA)
|
||||
->from("users/{$userB->id}/edit")
|
||||
->post("users/{$userB->id}/update/avatar", $data)
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNull($userB->fresh()->avatar);
|
||||
|
||||
$this->actingAs($userB)
|
||||
->from("users/{$userA->id}/edit")
|
||||
->post("users/{$userA->id}/update/avatar", $data)
|
||||
->assertRedirect("users/{$userA->id}/edit");
|
||||
|
||||
$this->assertNotNull($userA->fresh()->avatar);
|
||||
});
|
||||
|
||||
test('session page is not available for non database driver', function () {
|
||||
config(['session.driver' => 'array']);
|
||||
|
||||
$user = UserFactory::admin()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('users')
|
||||
->assertDontSee('User Sessions');
|
||||
|
||||
// this page should not be accessible if
|
||||
// database session driver is not being used
|
||||
$this->get("users/{$user->id}/sessions")->assertNotFound();
|
||||
});
|
||||
|
||||
test('invalidate session', function () {
|
||||
config(['session.driver' => 'database']);
|
||||
|
||||
Carbon::setTestNow(Carbon::now());
|
||||
|
||||
$user = UserFactory::admin()->withCredentials('foo', 'bar')->create();
|
||||
|
||||
$agent = $this->app['agent'];
|
||||
$device = $agent->device() ?: 'Unknown';
|
||||
$platform = $agent->platform() ?: 'Unknown';
|
||||
$browser = $agent->browser() ?: 'Unknown';
|
||||
|
||||
// Log-in manually to actually create session record in DB
|
||||
$this->post('/login', ['username' => 'foo', 'password' => 'bar']);
|
||||
|
||||
$sessions = $this->get("users/{$user->id}/sessions")
|
||||
->viewData('page')['props']['sessions'];
|
||||
|
||||
$this->assertCount(1, $sessions);
|
||||
$this->assertSame('127.0.0.1', $sessions[0]['ipAddress']);
|
||||
$this->assertSame($device, $sessions[0]['device']);
|
||||
$this->assertSame($platform, $sessions[0]['platform']);
|
||||
$this->assertSame($browser, $sessions[0]['browser']);
|
||||
|
||||
$sessionId = DB::table('sessions')->where('user_id', $user->id)->first()->id;
|
||||
$this->delete("users/{$user->id}/sessions/{$sessionId}/invalidate");
|
||||
|
||||
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
|
||||
});
|
||||
|
||||
test('admins can delete other users', function () {
|
||||
$user = UserFactory::user()->create();
|
||||
|
||||
$this->actingAsAdmin()
|
||||
->delete(route('users.destroy', $user))
|
||||
->assertRedirect('users');
|
||||
|
||||
$this->assertSessionHasSuccess('User deleted successfully.');
|
||||
|
||||
$this->assertNull($user->fresh());
|
||||
});
|
||||
|
||||
test('users without appropriate permissions cannot delete other users', function () {
|
||||
$roleA = RoleFactory::create();
|
||||
$roleB = RoleFactory::withPermissions('users.manage')->create();
|
||||
|
||||
$userA = UserFactory::role($roleA)->create();
|
||||
$userB = UserFactory::role($roleB)->create();
|
||||
|
||||
$this->actingAs($userA)
|
||||
->delete(route('users.destroy', $userB))
|
||||
->assertForbidden();
|
||||
|
||||
$this->assertNotNull($userB->fresh());
|
||||
|
||||
$this->actingAs($userB)
|
||||
->delete(route('users.destroy', $userA))
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertNull($userA->fresh());
|
||||
});
|
||||
|
||||
test('user cannot delete himself', function () {
|
||||
$this->beAdmin();
|
||||
|
||||
$this->delete(route('users.destroy', auth()->id()))
|
||||
->assertRedirect('/users');
|
||||
|
||||
$this->assertNotNull(auth()->user()->fresh());
|
||||
$this->assertSessionHasError('You cannot delete yourself.');
|
||||
});
|
||||
|
||||
function validUserParams(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'role_id' => Role::whereName('User')->first()->id,
|
||||
'status' => UserStatus::ACTIVE->value,
|
||||
'first_name' => 'foo',
|
||||
'last_name' => 'bar',
|
||||
'birthday' => Carbon::now()->subYears(25)->format('Y-m-d'),
|
||||
'phone' => '12345667',
|
||||
'address' => 'the address',
|
||||
'country_id' => 688, // Serbia,
|
||||
'email' => 'john@doe.com',
|
||||
'username' => 'johndoe',
|
||||
'password' => '123123123',
|
||||
'password_confirmation' => '123123123',
|
||||
], $overrides);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\UpdatesSettings;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
uses(UpdatesSettings::class);
|
||||
|
||||
test('unverified user sees verification notice', function () {
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('email/verify')
|
||||
->assertOk()
|
||||
->assertInertia(fn (Assert $page) => $page->component('auth/VerifyEmail'));
|
||||
});
|
||||
|
||||
test('already verified user is redirected from notice page', function () {
|
||||
$user = User::factory()->create(['email_verified_at' => Carbon::now()]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get('email/verify')
|
||||
->assertRedirect('/');
|
||||
});
|
||||
|
||||
test('signed verification link marks email as verified', function () {
|
||||
Event::fake([Verified::class]);
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$signedUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
Carbon::now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)]
|
||||
);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get($signedUrl)
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertNotNull($user->fresh()->email_verified_at);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
});
|
||||
|
||||
test('verification link with invalid signature fails', function () {
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('verification.verify', ['id' => $user->id, 'hash' => sha1($user->email)]))
|
||||
->assertStatus(403);
|
||||
|
||||
$this->assertNull($user->fresh()->email_verified_at);
|
||||
});
|
||||
|
||||
test('resend verification email', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => null]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('email/resend')
|
||||
->assertRedirect();
|
||||
|
||||
Notification::assertSentTo($user, VerifyEmail::class);
|
||||
});
|
||||
|
||||
test('resend does nothing for already verified user', function () {
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create(['email_verified_at' => Carbon::now()]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->post('email/resend')
|
||||
->assertRedirect('/');
|
||||
|
||||
Notification::assertNothingSent();
|
||||
});
|
||||
|
||||
test('guest cannot access verification routes', function () {
|
||||
$this->get('email/verify')->assertRedirect('login');
|
||||
$this->post('email/resend')->assertRedirect('login');
|
||||
});
|
||||
Reference in New Issue
Block a user