first commit

This commit is contained in:
2026-10-09 08:21:06 +02:00
parent db7456df87
commit d259c62304
3761 changed files with 896046 additions and 0 deletions
@@ -0,0 +1,60 @@
<?php
use App\Http\Filters\UserKeywordSearch;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('matches username', function () {
User::factory()->create(['username' => 'alice', 'email' => 'a@a.com']);
User::factory()->create(['username' => 'bob', 'email' => 'b@b.com']);
$results = applyFilter('alic')->get();
$this->assertCount(1, $results);
$this->assertSame('alice', $results->first()->username);
});
test('matches email', function () {
User::factory()->create(['email' => 'alice@example.com']);
User::factory()->create(['email' => 'bob@example.com']);
$results = applyFilter('alice@')->get();
$this->assertCount(1, $results);
});
test('matches first name and last name', function () {
User::factory()->create(['first_name' => 'Alice', 'last_name' => 'Cooper']);
User::factory()->create(['first_name' => 'Bob', 'last_name' => 'Dylan']);
$byFirst = applyFilter('Alic')->get();
$byLast = applyFilter('Coop')->get();
$this->assertCount(1, $byFirst);
$this->assertCount(1, $byLast);
$this->assertSame('Alice', $byFirst->first()->first_name);
$this->assertSame('Cooper', $byLast->first()->last_name);
});
test('returns empty when no match', function () {
User::factory()->create(['username' => 'alice']);
$results = applyFilter('xyzabc')->get();
$this->assertCount(0, $results);
});
function applyFilter(string $search)
{
$query = User::query();
(new UserKeywordSearch)($query, $search);
return $query;
}
@@ -0,0 +1,44 @@
<?php
use App\Http\Middleware\CheckIfBanned;
use App\Models\User;
use App\Support\Enum\UserStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('guest passes through', function () {
$request = Request::create('/');
$response = (new CheckIfBanned)->handle($request, fn () => 'ok');
$this->assertSame('ok', $response);
});
test('active user passes through', function () {
$user = User::factory()->create(['status' => UserStatus::ACTIVE]);
$request = Request::create('/');
$request->setUserResolver(fn () => $user);
$response = (new CheckIfBanned)->handle($request, fn () => 'ok');
$this->assertSame('ok', $response);
});
test('banned user is blocked', function () {
$user = User::factory()->create(['status' => UserStatus::BANNED]);
$request = Request::create('/');
$request->setUserResolver(fn () => $user);
$this->expectException(HttpException::class);
(new CheckIfBanned)->handle($request, fn () => 'ok');
});
@@ -0,0 +1,68 @@
<?php
use App\Http\Middleware\CheckPermissions;
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Facades\Tests\Setup\UserFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('guest is blocked', function () {
$request = Request::create('/');
$this->expectException(HttpException::class);
permissionMiddleware()->handle($request, fn () => 'ok', 'users.manage');
});
test('user without permission is blocked', function () {
$user = UserFactory::user()->create();
$this->be($user);
$this->expectException(HttpException::class);
permissionMiddleware()->handle(permissionRequestFor($user), fn () => 'ok', 'users.manage');
});
test('user with permission passes', function () {
$user = UserFactory::withPermissions('users.manage')->create();
$this->be($user);
$response = permissionMiddleware()->handle(permissionRequestFor($user), fn () => 'ok', 'users.manage');
$this->assertSame('ok', $response);
});
test('multiple permissions separated by pipe', function () {
$role = Role::factory()->create();
$role->attachPermission(Permission::where('name', 'users.manage')->first());
$role->attachPermission(Permission::where('name', 'roles.manage')->first());
$user = User::factory()->create(['role_id' => $role->id]);
$this->be($user);
$response = permissionMiddleware()->handle(permissionRequestFor($user), fn () => 'ok', 'users.manage|roles.manage');
$this->assertSame('ok', $response);
});
function permissionRequestFor($user): Request
{
$request = Request::create('/');
$request->setUserResolver(fn () => $user);
return $request;
}
function permissionMiddleware(): CheckPermissions
{
return new CheckPermissions(auth()->guard());
}
@@ -0,0 +1,53 @@
<?php
use App\Http\Middleware\CheckRole;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('guest is blocked', function () {
$this->expectException(HttpException::class);
roleMiddleware()->handle(Request::create('/'), fn () => 'ok', 'Admin');
});
test('user with matching role passes', function () {
$role = Role::factory()->create(['name' => 'Editor']);
$user = User::factory()->create(['role_id' => $role->id]);
$this->be($user);
$response = roleMiddleware()->handle(roleRequestFor($user), fn () => 'ok', 'Editor');
$this->assertSame('ok', $response);
});
test('user with wrong role is blocked', function () {
$role = Role::factory()->create(['name' => 'Editor']);
$user = User::factory()->create(['role_id' => $role->id]);
$this->be($user);
$this->expectException(HttpException::class);
roleMiddleware()->handle(roleRequestFor($user), fn () => 'ok', 'Admin');
});
function roleRequestFor($user): Request
{
$request = Request::create('/');
$request->setUserResolver(fn () => $user);
return $request;
}
function roleMiddleware(): CheckRole
{
return new CheckRole(auth()->guard());
}
@@ -0,0 +1,54 @@
<?php
use App\Http\Middleware\EnsureUserIsApproved;
use App\Models\User;
use App\Support\Enum\UserStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('guest passes through', function () {
$response = (new EnsureUserIsApproved)->handle(Request::create('/'), fn () => 'ok');
$this->assertSame('ok', $response);
});
test('active user passes through', function () {
$user = User::factory()->create(['status' => UserStatus::ACTIVE]);
$request = Request::create('/');
$request->setUserResolver(fn () => $user);
$response = (new EnsureUserIsApproved)->handle($request, fn () => 'ok');
$this->assertSame('ok', $response);
});
test('waiting user redirects to approval notice for web', function () {
Route::get('/approval-notice', fn () => 'notice')->name('approval.notice');
$user = User::factory()->create(['status' => UserStatus::WAITING_APPROVAL]);
$request = Request::create('/');
$request->setUserResolver(fn () => $user);
$response = (new EnsureUserIsApproved)->handle($request, fn () => 'ok');
$this->assertSame(302, $response->getStatusCode());
$this->assertStringContainsString('/approval-notice', $response->headers->get('Location'));
});
test('waiting user aborts for json request', function () {
$user = User::factory()->create(['status' => UserStatus::WAITING_APPROVAL]);
$request = Request::create('/');
$request->headers->set('Accept', 'application/json');
$request->setUserResolver(fn () => $user);
$this->expectException(HttpException::class);
(new EnsureUserIsApproved)->handle($request, fn () => 'ok');
});
@@ -0,0 +1,52 @@
<?php
use App\Http\Middleware\PasswordResetEnabled;
use App\Http\Middleware\RegistrationEnabled;
use App\Http\Middleware\TwoFactorEnabled;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
test('registration enabled aborts when disabled', function () {
$this->setSettings(['reg_enabled' => false]);
$this->expectException(NotFoundHttpException::class);
(new RegistrationEnabled)->handle(Request::create('/'), fn () => 'ok');
});
test('registration enabled passes when enabled', function () {
$this->setSettings(['reg_enabled' => true]);
$this->assertSame('ok', (new RegistrationEnabled)->handle(Request::create('/'), fn () => 'ok'));
});
test('password reset aborts when disabled', function () {
$this->setSettings(['forgot_password' => false]);
$this->expectException(NotFoundHttpException::class);
(new PasswordResetEnabled)->handle(Request::create('/'), fn () => 'ok');
});
test('password reset passes when enabled', function () {
$this->setSettings(['forgot_password' => true]);
$this->assertSame('ok', (new PasswordResetEnabled)->handle(Request::create('/'), fn () => 'ok'));
});
test('two factor aborts when disabled', function () {
$this->setSettings(['2fa.enabled' => false]);
$this->expectException(NotFoundHttpException::class);
(new TwoFactorEnabled)->handle(Request::create('/'), fn () => 'ok');
});
test('two factor passes when enabled', function () {
$this->setSettings(['2fa.enabled' => true]);
$this->assertSame('ok', (new TwoFactorEnabled)->handle(Request::create('/'), fn () => 'ok'));
});
@@ -0,0 +1,25 @@
<?php
use App\Http\Middleware\SocialLogin;
test('allows configured provider', function () {
config(['auth.social.providers' => ['github', 'google']]);
$response = $this->get('auth/github/login');
// Either the controller handles the request or another middleware
// intervenes — but the SocialLogin middleware itself should not 404.
$this->assertNotEquals(404, $response->getStatusCode());
});
test('middleware throws when provider not in config', function () {
config(['auth.social.providers' => ['github']]);
// Build a real route so $request->route()->parameter('provider') resolves.
Route::middleware(SocialLogin::class)
->get('/test-social/{provider}', fn ($provider) => 'ok');
$response = $this->get('/test-social/unknown');
$response->assertNotFound();
});
@@ -0,0 +1,13 @@
<?php
use App\Http\Middleware\UseApiGuard;
use Illuminate\Http\Request;
test('middleware switches default guard to sanctum and sets accept header', function () {
$request = Request::create('/');
$response = (new UseApiGuard(app('auth')))->handle($request, fn ($req) => $req);
$this->assertSame('application/json', $response->headers->get('Accept'));
$this->assertSame('sanctum', auth()->getDefaultDriver());
});
@@ -0,0 +1,54 @@
<?php
use App\Http\Resources\CountryResource;
use App\Models\Country;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('resource has expected keys', function () {
$country = Country::first();
$array = (new CountryResource($country))->toArray(new Request);
$this->assertSame([
'id', 'name', 'full_name', 'capital', 'citizenship', 'country_code',
'currency', 'currency_code', 'currency_sub_unit', 'currency_symbol',
'iso_3166_2', 'iso_3166_3', 'region_code', 'sub_region_code',
'eea', 'calling_code', 'flag',
], array_keys($array));
});
test('flag resolves to url when set', function () {
$country = Country::first();
$country->flag = 'UK.png';
$array = (new CountryResource($country))->toArray(new Request);
$this->assertStringContainsString('flags/UK.png', $array['flag']);
});
test('flag is null when missing', function () {
$country = Country::first();
$country->flag = null;
$array = (new CountryResource($country))->toArray(new Request);
$this->assertNull($array['flag']);
});
test('numeric casts', function () {
$country = Country::first();
$array = (new CountryResource($country))->toArray(new Request);
$this->assertIsInt($array['id']);
$this->assertIsInt($array['country_code']);
$this->assertIsInt($array['region_code']);
$this->assertIsBool($array['eea']);
});
@@ -0,0 +1,32 @@
<?php
use App\Http\Resources\PermissionResource;
use App\Models\Permission;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('resource has expected keys', function () {
$permission = Permission::factory()->create();
$array = (new PermissionResource($permission))->toArray(new Request);
$this->assertSame([
'id', 'name', 'display_name', 'description', 'removable',
'updated_at', 'created_at',
], array_keys($array));
});
test('id is int removable is bool', function () {
$permission = Permission::factory()->create(['removable' => 1]);
$array = (new PermissionResource($permission))->toArray(new Request);
$this->assertIsInt($array['id']);
$this->assertTrue($array['removable']);
});
@@ -0,0 +1,50 @@
<?php
use App\Http\Resources\RoleResource;
use App\Models\Permission;
use App\Models\Role;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Spatie\QueryBuilder\AllowedInclude;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('resource includes permissions when loaded', function () {
$role = Role::factory()->create();
$perm = Permission::factory()->create();
$role->attachPermission($perm);
$role->load('permissions');
$array = (new RoleResource($role))->toArray(new Request);
$this->assertNotEmpty($array['permissions']);
});
test('users count is int when present', function () {
$role = Role::factory()->create();
$role->users_count = '5';
$array = (new RoleResource($role))->toArray(new Request);
$this->assertSame(5, $array['users_count']);
});
test('users count is null when not set', function () {
$role = Role::factory()->create();
$array = (new RoleResource($role))->toArray(new Request);
$this->assertNull($array['users_count']);
});
test('allowed includes returns permissions and users count', function () {
$includes = RoleResource::allowedIncludes();
$this->assertContains('permissions', $includes);
$this->assertCount(2, $includes);
$this->assertTrue(collect($includes)->contains(fn ($i) => $i instanceof AllowedInclude || $i === 'permissions'));
});
@@ -0,0 +1,26 @@
<?php
use App\Http\Resources\SessionResource;
use Illuminate\Http\Request;
test('resource has expected keys', function () {
$session = (object) [
'id' => 'abc123',
'user_id' => 42,
'ip_address' => '127.0.0.1',
'user_agent' => 'PHPUnit',
'browser' => 'Chrome',
'platform' => 'macOS',
'device' => 'Desktop',
'last_activity' => '2026-01-01 00:00:00',
];
$array = (new SessionResource($session))->toArray(new Request);
$this->assertSame([
'id', 'user_id', 'ip_address', 'user_agent',
'browser', 'platform', 'device', 'last_activity',
], array_keys($array));
$this->assertSame(42, $array['user_id']);
});
@@ -0,0 +1,73 @@
<?php
use App\Http\Resources\UserResource;
use App\Models\Role;
use App\Models\User;
use Facades\Tests\Setup\UserFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('resource has expected keys', function () {
$user = User::factory()->create();
$array = (new UserResource($user))->toArray(fakeRequest($user));
$expected = [
'id', 'first_name', 'last_name', 'username', 'email', 'phone', 'avatar',
'address', 'country_id', 'role_id', 'status', 'birthday', 'last_login',
'two_factor_country_code', 'two_factor_phone', 'two_factor_options',
'email_verified_at', 'created_at', 'updated_at', 'country',
];
foreach ($expected as $key) {
$this->assertArrayHasKey($key, $array, "missing key: $key");
}
});
test('role is hidden without permission', function () {
$role = Role::factory()->create();
$user = User::factory()->create(['role_id' => $role->id]);
$user->load('role');
$serialized = serializeResource($user);
$this->assertArrayNotHasKey('role', $serialized);
});
test('role is visible with permission', function () {
$admin = UserFactory::withPermissions('roles.manage')->create();
$admin->load('role');
$serialized = serializeResource($admin);
$this->assertArrayHasKey('role', $serialized);
});
test('allowed includes', function () {
$this->assertSame(['role', 'country'], UserResource::allowedIncludes());
});
function fakeRequest(User $user): Request
{
$request = Request::create('/');
$request->setUserResolver(fn () => $user);
return $request;
}
/**
* Serialize the resource through JSON to filter out MissingValue placeholders.
*/
function serializeResource(User $user): array
{
return json_decode(
(new UserResource($user))->toResponse(fakeRequest($user))->getContent(),
true,
)['data'];
}
@@ -0,0 +1,40 @@
<?php
use App\Events\User\Approved;
use App\Listeners\Approve\SendApprovedNotification;
use App\Mail\UserApproved;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\UpdatesSettings;
uses(RefreshDatabase::class);
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('mail is sent when approval enabled', function () {
$this->setSettings(['approval.enabled' => true]);
Mail::fake();
$user = User::factory()->create();
(new SendApprovedNotification)->handle(new Approved($user));
Mail::assertQueued(UserApproved::class, fn ($mail) => $mail->hasTo($user->email));
});
test('mail is not sent when approval disabled', function () {
$this->setSettings(['approval.enabled' => false]);
Mail::fake();
$user = User::factory()->create();
(new SendApprovedNotification)->handle(new Approved($user));
Mail::assertNothingQueued();
});
@@ -0,0 +1,48 @@
<?php
use App\Listeners\Approve\SendWaitingApprovalNotification;
use App\Mail\WaitingApproval;
use App\Repositories\User\UserRepository;
use Facades\Tests\Setup\UserFactory;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\UpdatesSettings;
uses(RefreshDatabase::class);
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('mail is sent to each admin when approval enabled', function () {
$this->setSettings(['approval.enabled' => true]);
Mail::fake();
$admin1 = UserFactory::admin()->create();
$admin2 = UserFactory::admin()->create();
$regular = UserFactory::user()->create();
$listener = new SendWaitingApprovalNotification(app(UserRepository::class));
$listener->handle(new Verified($regular));
Mail::assertQueued(WaitingApproval::class, fn ($mail) => $mail->hasTo($admin1->email));
Mail::assertQueued(WaitingApproval::class, fn ($mail) => $mail->hasTo($admin2->email));
Mail::assertNotQueued(WaitingApproval::class, fn ($mail) => $mail->hasTo($regular->email));
});
test('mail is not sent when approval disabled', function () {
$this->setSettings(['approval.enabled' => false]);
Mail::fake();
UserFactory::admin()->create();
$user = UserFactory::user()->create();
$listener = new SendWaitingApprovalNotification(app(UserRepository::class));
$listener->handle(new Verified($user));
Mail::assertNothingQueued();
});
@@ -0,0 +1,26 @@
<?php
use App\Events\User\LoggedIn;
use App\Listeners\Login\UpdateLastLoginTimestamp;
use App\Models\User;
use App\Repositories\User\UserRepository;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('last login is set to now for authenticated user', function () {
Carbon::setTestNow(Carbon::now());
$user = User::factory()->create(['last_login' => null]);
$this->be($user);
$listener = new UpdateLastLoginTimestamp(app(UserRepository::class), auth()->guard());
$listener->handle(new LoggedIn);
$this->assertEquals(Carbon::now()->timestamp, $user->fresh()->last_login->timestamp);
});
@@ -0,0 +1,45 @@
<?php
use App\Listeners\Registration\SendSignUpNotification;
use App\Mail\UserRegistered;
use App\Repositories\User\UserRepository;
use Facades\Tests\Setup\UserFactory;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\UpdatesSettings;
uses(RefreshDatabase::class);
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('admins receive notification when signup emails enabled', function () {
$this->setSettings(['notifications_signup_email' => true]);
Mail::fake();
$admin = UserFactory::admin()->create();
$newUser = UserFactory::user()->create();
$listener = new SendSignUpNotification(app(UserRepository::class));
$listener->handle(new Registered($newUser));
Mail::assertQueued(UserRegistered::class, fn ($mail) => $mail->hasTo($admin->email));
});
test('no mail when signup emails disabled', function () {
$this->setSettings(['notifications_signup_email' => false]);
Mail::fake();
UserFactory::admin()->create();
$newUser = UserFactory::user()->create();
$listener = new SendSignUpNotification(app(UserRepository::class));
$listener->handle(new Registered($newUser));
Mail::assertNothingQueued();
});
@@ -0,0 +1,38 @@
<?php
use App\Listeners\Users\ActivateUser;
use App\Models\User;
use App\Repositories\User\UserRepository;
use App\Support\Enum\UserStatus;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\UpdatesSettings;
uses(RefreshDatabase::class);
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('user becomes active when approval disabled', function () {
$this->setSettings(['approval.enabled' => false]);
$user = User::factory()->create(['status' => UserStatus::UNCONFIRMED]);
$listener = new ActivateUser(app(UserRepository::class));
$listener->handle(new Verified($user));
$this->assertSame(UserStatus::ACTIVE, $user->fresh()->status);
});
test('user waits for approval when approval enabled', function () {
$this->setSettings(['approval.enabled' => true]);
$user = User::factory()->create(['status' => UserStatus::UNCONFIRMED]);
$listener = new ActivateUser(app(UserRepository::class));
$listener->handle(new Verified($user));
$this->assertSame(UserStatus::WAITING_APPROVAL, $user->fresh()->status);
});
@@ -0,0 +1,34 @@
<?php
use App\Events\User\Banned;
use App\Listeners\Users\InvalidateSessions;
use App\Models\User;
use App\Repositories\Session\SessionRepository;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('all user sessions are invalidated', function () {
$user = User::factory()->create();
DB::table('sessions')->insert([
'id' => Str::random(),
'user_id' => $user->id,
'ip_address' => fake()->ipv4,
'user_agent' => fake()->userAgent,
'payload' => 'foo',
'last_activity' => Carbon::now()->timestamp,
]);
$listener = new InvalidateSessions(app(SessionRepository::class));
$listener->handle(new Banned($user));
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
});
+24
View File
@@ -0,0 +1,24 @@
<?php
use App\Mail\ResetPassword;
test('subject includes app name', function () {
Setting::set('app_name', 'TestApp');
$mailable = new ResetPassword('the-token');
$mailable->build();
$mailable->assertHasSubject('[TestApp] '.__('Password Reset Request'));
});
test('uses reset password markdown view', function () {
$mailable = new ResetPassword('the-token');
$mailable->assertSeeInText('reset');
});
test('token is publicly accessible', function () {
$mailable = new ResetPassword('the-token');
$this->assertSame('the-token', $mailable->token);
});
+12
View File
@@ -0,0 +1,12 @@
<?php
use App\Mail\UserApproved;
test('subject includes app name', function () {
Setting::set('app_name', 'TestApp');
$mailable = new UserApproved;
$mailable->build();
$mailable->assertHasSubject('[TestApp] '.__('Your Account Has Been Approved'));
});
+28
View File
@@ -0,0 +1,28 @@
<?php
use App\Mail\UserRegistered;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('subject includes app name', function () {
Setting::set('app_name', 'TestApp');
$user = User::factory()->create();
$mailable = new UserRegistered($user);
$mailable->build();
$mailable->assertHasSubject('[TestApp] '.__('New User Registration'));
});
test('user is publicly accessible', function () {
$user = User::factory()->create();
$mailable = new UserRegistered($user);
$this->assertTrue($user->is($mailable->user));
});
+28
View File
@@ -0,0 +1,28 @@
<?php
use App\Mail\WaitingApproval;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('subject includes app name', function () {
Setting::set('app_name', 'TestApp');
$user = User::factory()->create();
$mailable = new WaitingApproval($user);
$mailable->build();
$mailable->assertHasSubject('[TestApp] '.__('User Approval'));
});
test('user is publicly accessible', function () {
$user = User::factory()->create();
$mailable = new WaitingApproval($user);
$this->assertTrue($user->is($mailable->user));
});
+165
View File
@@ -0,0 +1,165 @@
<?php
use App\Models\User;
use App\Support\Sidebar\Item;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Vanguard\Plugins\Contracts\Slot;
use Vanguard\Plugins\Plugin;
use Vanguard\Plugins\Vanguard;
use Vanguard\Plugins\Widget;
uses(RefreshDatabase::class);
afterEach(function () {
Vanguard::$slots = [];
Vanguard::$widgets = [];
});
test('slot handlers are registered per position', function () {
Vanguard::$slots = [];
$this->assertFalse(Vanguard::hasSlot('navbar:items'));
Vanguard::slot('navbar:items', FakeSlot::class);
$this->assertTrue(Vanguard::hasSlot('navbar:items'));
$this->assertSame([FakeSlot::class], Vanguard::slotHandlers('navbar:items'));
});
test('multiple handlers may share a position', function () {
Vanguard::$slots = [];
Vanguard::slot('navbar:items', FakeSlot::class);
Vanguard::slot('navbar:items', OtherFakeSlot::class);
$this->assertCount(2, Vanguard::slotHandlers('navbar:items'));
});
test('registering the same handler twice keeps a single entry', function () {
Vanguard::$slots = [];
// Handlers are declared as class names, so a repeat only ever means the
// service provider booted again: under Octane, or between tests, where the
// static registry outlives the container. Appending would render the slot
// once per boot.
Vanguard::slot('navbar:items', FakeSlot::class);
Vanguard::slot('navbar:items', FakeSlot::class);
$this->assertSame([FakeSlot::class], Vanguard::slotHandlers('navbar:items'));
});
test('registering the same widget twice keeps a single entry', function () {
Vanguard::$widgets = [];
Vanguard::widgets([new FakeWidget]);
Vanguard::widgets([new FakeWidget]);
$this->assertCount(1, Vanguard::availableWidgets(User::factory()->create()));
});
test('resolving a slot returns component and props', function () {
Vanguard::$slots = [];
Vanguard::slot('user:show', FakeSlot::class);
$this->assertSame([
[
'component' => 'fake::Component',
'props' => ['greeting' => 'hello world'],
],
], Vanguard::resolveSlot('user:show', ['greeting' => 'hello world']));
});
test('resolving an unregistered slot returns an empty array', function () {
Vanguard::$slots = [];
$this->assertSame([], Vanguard::resolveSlot('nope:nothing'));
});
test('widget serialises to component props and width', function () {
$widget = new FakeWidget;
$this->assertSame([
'component' => 'fake::Widget',
'props' => ['count' => 7],
'width' => '4',
], $widget->toArray());
});
test('widget without permissions authorizes everyone', function () {
$user = User::factory()->create();
$this->assertTrue((new FakeWidget)->authorize($user));
});
test('widget permissions may be a closure', function () {
$user = User::factory()->create();
$widget = (new FakeWidget)->permissions(fn () => false);
$this->assertFalse($widget->authorize($user));
});
test('plugin declares no assets by default', function () {
$plugin = new FakePlugin($this->app);
$this->assertSame([], $plugin->assets());
});
test('registry collects assets from plugins that declare them', function () {
Vanguard::$plugins = [
FakePlugin::class => new FakePlugin($this->app),
FakePluginWithAssets::class => new FakePluginWithAssets($this->app),
];
$this->assertSame(
[['js' => '/vendor/plugins/fake/plugin.js']],
Vanguard::assets()
);
});
class FakeSlot implements Slot
{
public function component(): string
{
return 'fake::Component';
}
public function data(array $context = []): array
{
return ['greeting' => $context['greeting'] ?? null];
}
}
class OtherFakeSlot extends FakeSlot {}
class FakeWidget extends Widget
{
public ?string $width = '4';
public function component(): string
{
return 'fake::Widget';
}
public function data(): array
{
return ['count' => 7];
}
}
class FakePlugin extends Plugin
{
public function sidebar(): ?Item
{
return null;
}
}
class FakePluginWithAssets extends Plugin
{
public function assets(): array
{
return ['js' => '/vendor/plugins/fake/plugin.js'];
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
use App\Presenters\Traits\Presentable;
use App\Presenters\UserPresenter;
use App\Support\Enum\UserStatus;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
beforeEach(function () {
$this->user = new PresentableUser;
});
test('test test name', function () {
$this->assertEquals('John Doe', $this->user->present()->name);
});
test('test name or email', function () {
$this->assertEquals('John Doe', $this->user->present()->nameOrEmail);
$this->user->first_name = '';
$this->assertEquals('Doe', $this->user->present()->nameOrEmail);
$this->user->last_name = '';
$this->assertEquals('john.doe@test.com', $this->user->present()->nameOrEmail);
});
test('test avatar', function () {
$this->assertEquals(url('assets/img/profile.png'), $this->user->present()->avatar);
$this->user->avatar = '//www.gravatar.com/avatar';
$this->assertEquals('//www.gravatar.com/avatar', $this->user->present()->avatar);
$this->user->avatar = 'http://somewebsite.com/avatar.png';
$this->assertEquals('http://somewebsite.com/avatar.png', $this->user->present()->avatar);
$this->user->avatar = 'foo.png';
$this->assertEquals(url('storage/upload/users/foo.png'), $this->user->present()->avatar);
});
test('test birthday', function () {
$this->assertEquals('N/A', $this->user->present()->birthday);
$this->user->birthday = Carbon::now()->subYears(20);
$this->assertEquals($this->user->birthday->toDateString(), $this->user->present()->birthday);
});
test('test full address', function () {
$this->assertEquals('Some Country', $this->user->present()->fullAddress);
$this->user->address = 'Street 123';
$this->assertEquals('Street 123, Some Country', $this->user->present()->fullAddress);
$this->user->country_id = null;
$this->user->country = null;
$this->assertEquals('Street 123', $this->user->present()->fullAddress);
$this->user->address = '';
$this->assertEquals('N/A', $this->user->present()->fullAddress);
});
test('test last login', function () {
$this->assertEquals('N/A', $this->user->present()->lastLogin);
$date = Carbon::now()->subDay(1);
$this->user->last_login = $date->copy();
$this->assertEquals($date->diffForHumans(), $this->user->present()->lastLogin);
});
test('test label class', function () {
$this->assertEquals('success', $this->user->present()->labelClass);
$this->user->status = UserStatus::BANNED;
$this->assertEquals('danger', $this->user->present()->labelClass);
$this->user->status = UserStatus::UNCONFIRMED;
$this->assertEquals('warning', $this->user->present()->labelClass);
});
class PresentableUser extends Model
{
use Presentable;
protected $presenter = UserPresenter::class;
public $first_name = 'John';
public $last_name = 'Doe';
public $email = 'john.doe@test.com';
public $avatar = null;
public $birthday = null;
public $country_id = 1;
public $country;
public $address = '';
public $last_login = null;
public $status = UserStatus::ACTIVE;
public function __construct()
{
$this->country = new StubCountry;
}
}
class StubCountry
{
public $id = 1;
public $name = 'Some Country';
}
@@ -0,0 +1,20 @@
<?php
use App\Models\Country;
use App\Repositories\Country\EloquentCountry;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->repo = app(EloquentCountry::class);
});
test('lists', function () {
$countries = Country::factory()->times(8)->create();
$countries = $countries->sortBy(function ($country) {
return $country->name;
})->pluck('name', 'id');
$this->assertEquals($countries->toArray(), $this->repo->lists()->toArray());
});
@@ -0,0 +1,81 @@
<?php
use App\Events\Permission\Created;
use App\Events\Permission\Deleted;
use App\Events\Permission\Updated;
use App\Models\Permission;
use App\Repositories\Permission\EloquentPermission;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->repo = app(EloquentPermission::class);
});
test('all', function () {
$permissions = Permission::factory()->times(4)->create();
$this->assertEquals($permissions->toArray(), $this->repo->all()->toArray());
});
test('create permission', function () {
Event::fake([
Created::class,
]);
$data = getPermissionStubData();
$perm = $this->repo->create($data);
$this->assertDatabaseHas('permissions', $data + ['id' => $perm->id]);
Event::assertDispatched(Created::class);
});
test('update permission', function () {
Event::fake([
Updated::class,
]);
Cache::put('foo', 'bar');
$data = getPermissionStubData();
$perm = Permission::factory()->create();
$this->repo->update($perm->id, $data);
$this->assertDatabaseHas('permissions', $data + ['id' => $perm->id])
->assertNull(Cache::get('foo'));
Event::assertDispatched(Updated::class);
});
test('delete permission', function () {
Event::fake([
Deleted::class,
]);
Cache::put('foo', 'bar');
$perm = Permission::factory()->create();
$this->repo->delete($perm->id);
$this->assertDatabaseMissing('permissions', ['id' => $perm->id])
->assertNull(Cache::get('foo'));
Event::assertDispatched(Deleted::class);
});
function getPermissionStubData(): array
{
return [
'name' => Str::random(5),
'display_name' => Str::random(5),
'description' => 'foo',
'removable' => true,
];
}
@@ -0,0 +1,105 @@
<?php
use App\Events\Role\Created;
use App\Events\Role\Deleted;
use App\Events\Role\Updated;
use App\Models\Permission;
use App\Models\Role;
use App\Repositories\Role\EloquentRole;
use Facades\Tests\Setup\RoleFactory;
use Facades\Tests\Setup\UserFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->repo = app(EloquentRole::class);
});
test('all', function () {
$roles = Role::factory()->times(4)->create();
$this->assertEquals(
$roles->toArray(),
$this->repo->all()->toArray()
);
});
test('get all with users count', function () {
$roleA = RoleFactory::create();
$roleB = RoleFactory::create();
$roleC = RoleFactory::create();
UserFactory::role($roleA)->create();
UserFactory::role($roleA)->create();
UserFactory::role($roleB)->create();
$roleA->users_count = 2;
$roleB->users_count = 1;
$roleC->users_count = 0;
$this->assertEquals(
[$roleA->toArray(), $roleB->toArray(), $roleC->toArray()],
$this->repo->getAllWithUsersCount()->toArray()
);
});
test('create', function () {
Event::fake([
Created::class,
]);
$data = ['name' => 'foo', 'display_name' => 'Foo'];
$role = $this->repo->create($data);
$this->assertDatabaseHas('roles', $data + ['id' => $role->id]);
Event::assertDispatched(Created::class);
});
test('update', function () {
Event::fake([
Updated::class,
]);
$role = Role::factory()->create();
$data = ['name' => 'foo', 'display_name' => 'Foo'];
$this->repo->update($role->id, $data);
$this->assertDatabaseHas('roles', $data + ['id' => $role->id]);
Event::assertDispatched(Updated::class);
});
test('delete role', function () {
Event::fake([
Deleted::class,
]);
$role = Role::factory()->create();
$this->repo->delete($role->id);
$this->assertDatabaseMissing('roles', ['id' => $role->id]);
Event::assertDispatched(Deleted::class);
});
test('update permissions', function () {
$role = Role::factory()->create();
$permissions = Permission::factory()->times(2)->create();
$this->repo->updatePermissions($role->id, $permissions->pluck('id')->toArray());
$this->assertDatabaseHas('permission_role', ['role_id' => $role->id, 'permission_id' => $permissions[0]->id]);
$this->assertDatabaseHas('permission_role', ['role_id' => $role->id, 'permission_id' => $permissions[1]->id]);
});
test('lists', function () {
$roles = Role::factory()->times(4)->create();
$roles = $roles->pluck('display_name', 'id');
$this->assertEquals($roles->toArray(), $this->repo->lists()->toArray());
});
@@ -0,0 +1,120 @@
<?php
use App\Models\User;
use App\Repositories\Session\DbSession;
use Carbon\Carbon;
use Faker\Generator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->repo = app(DbSession::class);
});
test('get user session', function () {
$user = User::factory()->create();
Carbon::setTestNow(Carbon::now());
$data1 = getSessionStubData($user);
$data2 = getSessionStubData($user);
DB::table('sessions')->insert($data1);
DB::table('sessions')->insert($data2);
$expected = collect([
(object) addAddMissingFields($data1),
(object) addAddMissingFields($data2),
]);
$expected = $expected->sortBy('id')->keyBy('id')->toArray();
$actual = $this->repo->getUserSessions($user->id)
->sortBy('id')
->keyBy('id')
->toArray();
$this->assertEquals($expected, $actual);
});
function addAddMissingFields(array $data)
{
$agent = app('agent');
$agent->setUserAgent($data['user_agent']);
return array_merge($data, [
'browser' => $agent->browser(),
'platform' => $agent->platform(),
'device' => $agent->device(),
'last_activity' => Carbon::createFromTimestamp($data['last_activity']),
]);
}
test('if get user sessions will return active sessions only', function () {
$user = User::factory()->create();
Carbon::setTestNow(Carbon::now());
$data1 = getSessionStubData($user);
$data2 = getSessionStubData($user);
$data2['last_activity'] = Carbon::now()->subMinutes(config('session.lifetime') + 1)->timestamp;
DB::table('sessions')->insert($data1);
DB::table('sessions')->insert($data2);
$expected = collect([
(object) addAddMissingFields($data1),
]);
$expected = $expected->sortBy('id')->keyBy('id')->toArray();
$actual = $this->repo->getUserSessions($user->id)
->sortBy('id')
->keyBy('id')
->toArray();
$this->assertEquals($expected, $actual);
});
test('invalidate user session', function () {
$user = User::factory()->create([
'remember_token' => Str::random(60),
]);
$data = getSessionStubData($user);
DB::table('sessions')->insert($data);
$this->repo->invalidateSession($data['id']);
$this->assertDatabaseMissing('sessions', $data)
->assertDatabaseHas('users', ['remember_token' => null]);
});
test('invalidate all sessions for user', function () {
$user = User::factory()->create([
'remember_token' => Str::random(60),
]);
$data = getSessionStubData($user);
DB::table('sessions')->insert($data);
$this->repo->invalidateAllSessionsForUser($user->id);
$this->assertDatabaseMissing('sessions', ['user_id' => $user->id])
->assertDatabaseHas('users', ['remember_token' => null]);
});
function getSessionStubData($user)
{
$faker = app(Generator::class);
return [
'id' => Str::random(),
'user_id' => $user->id,
'ip_address' => $faker->ipv4,
'user_agent' => $faker->userAgent,
'payload' => 'foo',
'last_activity' => Carbon::now()->timestamp,
];
}
@@ -0,0 +1,392 @@
<?php
use App\Models\Role;
use App\Models\User;
use App\Repositories\User\EloquentUser;
use App\Support\Enum\UserStatus;
use Carbon\Carbon;
use Facades\Tests\Setup\UserFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Illuminate\Testing\Assert;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->repo = app(EloquentUser::class);
});
test('test find', function () {
$user = User::factory()->create();
$this->assertTrue($user->is($this->repo->findByEmail($user->email)));
$this->assertNull($this->repo->find(123));
});
test('test find by email', function () {
$user = User::factory()->create();
$this->assertTrue($user->is($this->repo->findByEmail($user->email)));
$this->assertNull($this->repo->findByEmail('foo@bar.com'));
});
test('test find by social id', function () {
$user = User::factory()->create();
DB::table('social_logins')->insert([
'user_id' => $user->id,
'provider' => 'foo',
'provider_id' => '123',
'avatar' => '',
'created_at' => Carbon::now(),
]);
Assert::assertArraySubset(
$user->toArray(),
$this->repo->findBySocialId('foo', '123')->toArray()
);
$this->assertNull($this->repo->findBySocialId('bar', '111'));
});
test('test find by session id', function () {
$user = User::factory()->create();
$sessionId = Str::random(40);
DB::table('sessions')->insert([
'id' => $sessionId,
'user_id' => $user->id,
'ip_address' => '127.0.0.1',
'user_agent' => 'foo',
'payload' => Str::random(),
'last_activity' => Carbon::now(),
]);
Assert::assertArraySubset(
$user->toArray(),
$this->repo->findBySessionId($sessionId)->toArray()
);
});
test('test create', function () {
$data = User::factory()->make()->toArray();
$this->repo->create($data + ['password' => 'foo']);
$this->assertDatabaseHas('users', $data);
});
test('test associate facebook account to a user', function () {
$user = User::factory()->create();
Carbon::setTestNow(Carbon::now());
$socialUser = new Laravel\Socialite\One\User;
$socialUser->map([
'id' => '123',
'avatar' => 'foo',
'avatar_original' => 'foo?width=1920',
]);
$this->repo->associateSocialAccountForUser($user->id, 'facebook', $socialUser);
$this->assertDatabaseHas('social_logins', [
'user_id' => $user->id,
'provider' => 'facebook',
'provider_id' => '123',
'avatar' => 'foo?width=150',
'created_at' => Carbon::now(),
]);
Carbon::setTestNow(null);
});
test('test associate twitter account to a user', function () {
$user = User::factory()->create();
Carbon::setTestNow(Carbon::now());
$socialUser = new Laravel\Socialite\One\User;
$socialUser->map([
'id' => '123',
'avatar' => 'foo_normal.jpg',
]);
$this->repo->associateSocialAccountForUser($user->id, 'twitter', $socialUser);
$this->assertDatabaseHas('social_logins', [
'user_id' => $user->id,
'provider' => 'twitter',
'provider_id' => '123',
'avatar' => 'foo_200x200.jpg',
'created_at' => Carbon::now(),
]);
Carbon::setTestNow(null);
});
test('test associate google account to a user', function () {
$user = User::factory()->create();
Carbon::setTestNow(Carbon::now());
$socialUser = new Laravel\Socialite\One\User;
$socialUser->map([
'id' => '123',
'avatar' => 'avatar.jpg',
'avatar_original' => 'avatar_original.jpg',
]);
$this->repo->associateSocialAccountForUser($user->id, 'google', $socialUser);
$this->assertDatabaseHas('social_logins', [
'user_id' => $user->id,
'provider' => 'google',
'provider_id' => '123',
'avatar' => 'avatar_original.jpg?sz=150',
'created_at' => Carbon::now(),
]);
Carbon::setTestNow(null);
});
test('test paginate', function () {
$users = User::factory()->times(5)->create();
$users = $users->sortByDesc('id')->values();
$result = $this->repo->paginate(2)->toArray();
$this->assertEquals(2, count($result['data']));
$this->assertEquals(5, $result['total']);
Assert::assertArraySubset($users[0]->toArray(), $result['data'][0]);
Assert::assertArraySubset($users[1]->toArray(), $result['data'][1]);
});
test('test paginate with status', function () {
User::factory()->times(3)->create();
User::factory()->create(['status' => UserStatus::BANNED]);
$active = $this->repo->paginate(2, null, UserStatus::ACTIVE->value)->toArray();
$banned = $this->repo->paginate(2, null, UserStatus::BANNED->value)->toArray();
$this->assertEquals(2, count($active['data']));
$this->assertEquals(3, $active['total']);
$this->assertEquals(1, count($banned['data']));
$this->assertEquals(1, $banned['total']);
});
test('test paginate with search', function () {
User::factory()->create([
'first_name' => 'John',
'last_name' => 'Doe',
'username' => 'jdoe',
'email' => 'joe@test.com',
]);
User::factory()->create([
'first_name' => 'Jane',
'last_name' => 'Doe',
'username' => 'janedoe',
'email' => 'jane@doe.com',
]);
User::factory()->create([
'first_name' => 'Milos',
'last_name' => 'Stojanovic',
'email' => 'test@test.com',
]);
$this->assertEquals(2, $this->repo->paginate(25, 'doe')->total());
$this->assertEquals(1, $this->repo->paginate(25, 'Milos')->total());
$this->assertEquals(2, $this->repo->paginate(25, 'test')->total());
$this->assertEquals(2, $this->repo->paginate(25, 'an')->total());
});
test('test update user', function () {
$user = User::factory()->create();
$data = [
'first_name' => 'John',
'last_name' => 'Doe',
'username' => 'foo',
'email' => 'test@test.com',
];
$this->repo->update($user->id, $data);
$this->assertDatabaseHas('users', $data + ['id' => $user->id]);
});
test('test update when provided country is zero', function () {
$user = User::factory()->create();
$data = [
'first_name' => 'John',
'last_name' => 'Doe',
'username' => 'foo',
'email' => 'test@test.com',
'country_id' => 0,
];
$this->repo->update($user->id, $data);
$this->assertDatabaseHas('users', array_merge($data, ['id' => $user->id, 'country_id' => null]));
});
test('test delete user', function () {
$user = User::factory()->create();
$this->repo->delete($user->id);
$this->assertDatabaseMissing('users', ['id' => $user->id]);
});
test('test count users', function () {
User::factory()->times(7)->create();
$this->assertEquals(7, $this->repo->count());
});
test('test new users count', function () {
Carbon::setTestNow(Carbon::now()->startOfMonth()->subMonth());
User::factory()->times(3)->create();
Carbon::setTestNow(null);
User::factory()->times(5)->create();
$this->assertEquals(5, $this->repo->newUsersCount());
});
test('test count by status', function () {
User::factory()->times(3)->create();
User::factory()->create(['status' => UserStatus::BANNED]);
User::factory()->times(2)->create(['status' => UserStatus::UNCONFIRMED]);
$this->assertEquals(3, $this->repo->countByStatus(UserStatus::ACTIVE));
$this->assertEquals(1, $this->repo->countByStatus(UserStatus::BANNED));
$this->assertEquals(2, $this->repo->countByStatus(UserStatus::UNCONFIRMED));
});
test('test latest', function () {
Carbon::setTestNow(now()->subDay());
$user4 = User::factory()->create();
Carbon::setTestNow(now()->subMinutes(3));
$user3 = User::factory()->create();
Carbon::setTestNow(now()->subMinutes(2));
$user2 = User::factory()->create();
Carbon::setTestNow(now()->subMinutes(1));
$user1 = User::factory()->create();
$latestTwo = $this->repo->latest(2);
$latestFour = $this->repo->latest(4);
$this->assertEquals(2, count($latestTwo));
$this->assertEquals(4, count($latestFour));
Assert::assertArraySubset($user4->toArray(), $latestTwo[0]->toArray());
Assert::assertArraySubset($user3->toArray(), $latestTwo[1]->toArray());
Assert::assertArraySubset($user1->toArray(), $latestFour[3]->toArray());
});
test('test count of new users per month', function () {
$from = now()->startOfYear();
Carbon::setTestNow($from);
User::factory()->times(2)->create();
Carbon::setTestNow($from->copy()->addMonths(2));
User::factory()->times(4)->create();
Carbon::setTestNow($from->copy()->addMonths(6));
User::factory()->times(2)->create();
Carbon::setTestNow($from->copy()->addMonths(7));
User::factory()->times(1)->create();
Carbon::setTestNow($from->copy()->addMonths(10));
User::factory()->times(4)->create();
Carbon::setTestNow(null);
$currentYear = now()->year;
$expected = [
"January {$currentYear}" => 2,
"February {$currentYear}" => 0,
"March {$currentYear}" => 4,
"April {$currentYear}" => 0,
"May {$currentYear}" => 0,
"June {$currentYear}" => 0,
"July {$currentYear}" => 2,
"August {$currentYear}" => 1,
"September {$currentYear}" => 0,
"October {$currentYear}" => 0,
"November {$currentYear}" => 4,
"December {$currentYear}" => 0,
];
$usersPerMonth = $this->repo->countOfNewUsersPerMonthPerRole(
now()->startOfYear(),
now()->endOfYear()
);
$this->assertEquals($expected, $usersPerMonth);
});
test('test get users with specific role', function () {
$this->artisan('db:seed --class=RolesSeeder');
$admin = UserFactory::admin()->create();
$user = UserFactory::user()->create();
$result = $this->repo->getUsersWithRole('Admin');
$this->assertEquals(1, $result->count());
$this->assertTrue($admin->is($result[0]));
$result = $this->repo->getUsersWithRole('User');
$this->assertEquals(1, $result->count());
$this->assertTrue($user->is($result[0]));
});
test('test set role', function () {
$this->artisan('db:seed --class=RolesSeeder');
$user = UserFactory::user()->create();
$role = Role::where('name', 'Admin')->first();
$this->repo->setRole($user->id, $role->id);
$this->assertDatabaseHas('users', [
'role_id' => $role->id,
'id' => $user->id,
]);
});
test('test switch roles for a user', function () {
$this->artisan('db:seed --class=RolesSeeder');
$adminRole = Role::where('name', 'Admin')->first();
$userA = UserFactory::user()->create();
$userB = UserFactory::user()->create();
$this->repo->switchRolesForUsers($userA->role_id, $adminRole->id);
$this->assertDatabaseHas('users', [
'role_id' => $adminRole->id,
'id' => $userA->id,
]);
$this->assertDatabaseHas('users', [
'role_id' => $adminRole->id,
'id' => $userB->id,
]);
});
@@ -0,0 +1,62 @@
<?php
use App\Rules\ValidPermissionName;
test('passes for valid names', function (string $name) {
$rule = new ValidPermissionName;
$this->assertTrue($rule->passes('name', $name));
})->with('validNames');
test('fails for invalid names', function (mixed $name) {
$rule = new ValidPermissionName;
$this->assertFalse($rule->passes('name', $name));
})->with('invalidNames');
test('validate calls fail with translated message when invalid', function () {
$rule = new ValidPermissionName;
$messages = [];
$rule->validate('name', 'has space', function ($msg) use (&$messages) {
$messages[] = $msg;
});
$this->assertNotEmpty($messages);
});
test('validate does not call fail when valid', function () {
$rule = new ValidPermissionName;
$called = false;
$rule->validate('name', 'users.manage', function () use (&$called) {
$called = true;
});
$this->assertFalse($called);
});
test('to string returns regex form', function () {
$rule = new ValidPermissionName;
$this->assertStringStartsWith('regex:', (string) $rule);
});
dataset('validNames', function () {
return [
'alphanumeric' => ['users123'],
'dot-separated' => ['users.manage'],
'underscore' => ['users_manage'],
'hyphen' => ['users-manage'],
'numeric string' => ['12345'],
];
});
dataset('invalidNames', function () {
return [
'space' => ['has space'],
'special char' => ['users@manage'],
'slash' => ['users/manage'],
'array' => [['users.manage']],
];
});
@@ -0,0 +1,106 @@
<?php
use App\Models\Role;
use App\Models\User;
use App\Repositories\Role\RoleRepository;
use App\Services\Auth\Social\SocialManager;
use App\Support\Enum\UserStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Socialite\Contracts\User as SocialUser;
use Tests\UpdatesSettings;
uses(RefreshDatabase::class);
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('associates existing user when email matches', function () {
$existing = User::factory()->create(['email' => 'foo@bar.com']);
$socialUser = makeSocialUser('foo@bar.com', 'John Doe');
$manager = app(SocialManager::class);
$user = $manager->associate($socialUser, 'facebook');
$this->assertTrue($existing->is($user));
$this->assertDatabaseHas('social_logins', [
'user_id' => $user->id,
'provider' => 'facebook',
]);
});
test('creates new user when email does not exist', function () {
$this->setSettings(['approval.enabled' => false]);
$socialUser = makeSocialUser('new@bar.com', 'Jane Smith');
$manager = app(SocialManager::class);
$user = $manager->associate($socialUser, 'google');
$this->assertSame('new@bar.com', $user->email);
$this->assertSame('Jane', $user->first_name);
$this->assertSame('Smith', $user->last_name);
$this->assertSame(UserStatus::ACTIVE, $user->status);
$this->assertNotNull($user->email_verified_at);
$this->assertDatabaseHas('social_logins', [
'user_id' => $user->id,
'provider' => 'google',
]);
});
test('new user status is waiting approval when approval enabled', function () {
$this->setSettings(['approval.enabled' => true]);
$socialUser = makeSocialUser('newer@bar.com', 'Foo Bar');
$manager = app(SocialManager::class);
$user = $manager->associate($socialUser, 'google');
$this->assertSame(UserStatus::WAITING_APPROVAL, $user->status);
});
test('single name is parsed without last name', function () {
$socialUser = makeSocialUser('single@bar.com', 'Cher');
$manager = app(SocialManager::class);
$user = $manager->associate($socialUser, 'facebook');
$this->assertSame('Cher', $user->first_name);
$this->assertSame('', $user->last_name);
});
test('new user gets default user role', function () {
$socialUser = makeSocialUser('role@bar.com', 'Role User');
$manager = app(SocialManager::class);
$user = $manager->associate($socialUser, 'facebook');
$this->assertSame(
app(RoleRepository::class)->findByName(Role::DEFAULT_USER_ROLE)->id,
$user->role_id,
);
});
test('facebook avatar url is resized', function () {
$socialUser = makeSocialUser('fb@bar.com', 'FB User');
$socialUser->avatar_original = 'https://avatar.fb/me?width=1920';
$manager = app(SocialManager::class);
$user = $manager->associate($socialUser, 'facebook');
$this->assertSame('https://avatar.fb/me?width=150', $user->avatar);
});
function makeSocialUser(string $email, string $name): SocialUser
{
$socialUser = Mockery::mock(SocialUser::class);
$socialUser->shouldReceive('getId')->andReturn((string) fake()->randomNumber());
$socialUser->shouldReceive('getEmail')->andReturn($email);
$socialUser->shouldReceive('getName')->andReturn($name);
$socialUser->shouldReceive('getAvatar')->andReturn('https://example.com/avatar.jpg');
$socialUser->avatar_original = 'https://example.com/avatar.jpg';
return $socialUser;
}
@@ -0,0 +1,10 @@
<?php
use App\Services\Auth\TwoFactor\Facade;
test('facade accessor is authy', function () {
$method = new ReflectionMethod(Facade::class, 'getFacadeAccessor');
$method->setAccessible(true);
$this->assertSame('authy', $method->invoke(null));
});
@@ -0,0 +1,78 @@
<?php
use App\Models\Permission;
use App\Models\Role;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('attach and detach permission', function () {
$role = Role::factory()->create();
$permission = Permission::factory()->create();
$role->attachPermission($permission);
$this->assertTrue($role->fresh()->hasPermission($permission->name));
$role->detachPermission($permission);
$this->assertFalse($role->fresh()->hasPermission($permission->name));
});
test('attach permissions supports id and object array', function () {
$role = Role::factory()->create();
$p1 = Permission::factory()->create();
$p2 = Permission::factory()->create();
$role->attachPermissions([$p1, ['id' => $p2->id]]);
$this->assertTrue($role->fresh()->hasPermission($p1->name));
$this->assertTrue($role->fresh()->hasPermission($p2->name));
});
test('sync permissions replaces permissions', function () {
$role = Role::factory()->create();
$p1 = Permission::factory()->create();
$p2 = Permission::factory()->create();
$role->attachPermission($p1);
$role->syncPermissions([$p2->id]);
$fresh = $role->fresh();
$this->assertFalse($fresh->hasPermission($p1->name));
$this->assertTrue($fresh->hasPermission($p2->name));
});
test('save permissions with empty array detaches all', function () {
$role = Role::factory()->create();
$p1 = Permission::factory()->create();
$role->attachPermission($p1);
$role->savePermissions([]);
$this->assertCount(0, $role->fresh()->permissions);
});
test('cached permissions are returned', function () {
$role = Role::factory()->create();
$p1 = Permission::factory()->create();
$role->attachPermission($p1);
$cached = $role->cachedPermissions();
$this->assertTrue($cached->contains('name', $p1->name));
});
test('save clears cache', function () {
$role = Role::factory()->create();
$p1 = Permission::factory()->create();
$role->attachPermission($p1);
$role->cachedPermissions();
$role->detachPermission($p1);
$role->save();
$this->assertFalse($role->cachedPermissions()->contains('name', $p1->name));
});
@@ -0,0 +1,31 @@
<?php
use App\Models\Permission;
use App\Models\Role;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('has role returns true when matching role', function () {
$role = Role::factory()->create(['name' => 'Editor']);
$user = User::factory()->create(['role_id' => $role->id]);
$this->assertTrue($user->hasRole('Editor'));
$this->assertFalse($user->hasRole('Admin'));
});
test('has permission returns true when role has permission', function () {
$role = Role::factory()->create();
$permission = Permission::factory()->create(['name' => 'something.do']);
$role->attachPermission($permission);
$user = User::factory()->create(['role_id' => $role->id]);
$this->assertTrue($user->hasPermission('something.do'));
$this->assertFalse($user->hasPermission('something.else'));
});
@@ -0,0 +1,53 @@
<?php
use App\Models\User;
use App\Support\Authorization\PasswordChangeManager;
use Illuminate\Cache\CacheManager;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\UpdatesSettings;
uses(RefreshDatabase::class);
uses(UpdatesSettings::class);
beforeEach(function () {
$this->artisan('db:seed');
$this->manager = new PasswordChangeManager(app(CacheManager::class));
});
test('force password change returns false when feature disabled', function () {
$this->setSettings(['password-change.enabled' => false]);
$user = User::factory()->create(['force_password_change' => true]);
$this->assertFalse($this->manager->forcePasswordChange($user));
$this->assertFalse($this->manager->isPasswordChangeForcedForUser($user));
});
test('force password change returns false when user flag false', function () {
$this->setSettings(['password-change.enabled' => true]);
$user = User::factory()->create(['force_password_change' => false]);
$this->assertFalse($this->manager->forcePasswordChange($user));
});
test('force password change sets cache when enabled and flagged', function () {
$this->setSettings(['password-change.enabled' => true]);
$user = User::factory()->create(['force_password_change' => true]);
$this->assertTrue($this->manager->forcePasswordChange($user));
$this->assertTrue($this->manager->isPasswordChangeForcedForUser($user));
});
test('lift password change request clears cache', function () {
$this->setSettings(['password-change.enabled' => true]);
$user = User::factory()->create(['force_password_change' => true]);
$this->manager->forcePasswordChange($user);
$this->manager->liftPasswordChangeRequest($user);
$this->assertFalse($this->manager->isPasswordChangeForcedForUser($user));
});
+64
View File
@@ -0,0 +1,64 @@
<?php
use App\Models\User;
use App\Support\Demo;
beforeEach(function () {
config([
'demo.enabled' => true,
'demo.protected_users' => ['admin'],
'demo.message' => null,
]);
});
test('nobody is protected while demo mode is off', function () {
config(['demo.enabled' => false]);
$this->assertFalse(Demo::enabled());
$this->assertFalse(Demo::isProtectedUser(new User(['username' => 'admin'])));
});
test('a guest is never protected', function () {
$this->assertFalse(Demo::isProtectedUser(null));
});
test('a protected account is matched on its username', function () {
$this->assertTrue(Demo::isProtectedUser(new User(['username' => 'admin'])));
});
test('a protected account is matched on its email', function () {
config(['demo.protected_users' => ['admin@example.com']]);
$this->assertTrue(Demo::isProtectedUser(new User(['email' => 'admin@example.com'])));
});
test('matching ignores case on both sides', function () {
config(['demo.protected_users' => ['ADMIN']]);
$this->assertTrue(Demo::isProtectedUser(new User(['username' => 'Admin'])));
});
test('an ordinary account is not protected', function () {
$this->assertFalse(Demo::isProtectedUser(new User([
'username' => 'jane',
'email' => 'jane@example.com',
])));
});
test('a user without a username is not protected by an empty match', function () {
config(['demo.protected_users' => ['']]);
$this->assertFalse(Demo::isProtectedUser(new User(['email' => 'jane@example.com'])));
});
test('the configured message overrides every reason', function () {
config(['demo.message' => 'Nope.']);
$this->assertSame('Nope.', Demo::message());
$this->assertSame('Nope.', Demo::message('Something more specific.'));
});
test('a reason falls back to the generic refusal', function () {
$this->assertSame('This action is disabled in the demo.', Demo::message());
$this->assertSame('Something more specific.', Demo::message('Something more specific.'));
});
@@ -0,0 +1,25 @@
<?php
use App\Support\Enum\UserStatus;
use Tests\UpdatesSettings;
uses(UpdatesSettings::class);
test('lists returns active banned unconfirmed when approval disabled', function () {
$this->setSettings(['approval.enabled' => false]);
$statuses = UserStatus::lists();
$this->assertArrayHasKey(UserStatus::ACTIVE->value, $statuses);
$this->assertArrayHasKey(UserStatus::BANNED->value, $statuses);
$this->assertArrayHasKey(UserStatus::UNCONFIRMED->value, $statuses);
$this->assertArrayNotHasKey(UserStatus::WAITING_APPROVAL->value, $statuses);
});
test('lists includes waiting approval when approval enabled', function () {
$this->setSettings(['approval.enabled' => true]);
$statuses = UserStatus::lists();
$this->assertArrayHasKey(UserStatus::WAITING_APPROVAL->value, $statuses);
});
+24
View File
@@ -0,0 +1,24 @@
<?php
use App\Support\Locale;
test('flag url returns url for known locale', function () {
$this->assertStringContainsString('/flags/GB.png', Locale::flagUrl('en'));
$this->assertStringContainsString('/flags/DE.png', Locale::flagUrl('de'));
$this->assertStringContainsString('/flags/RS.png', Locale::flagUrl('sr'));
});
test('flag url returns null for unknown locale', function () {
$this->assertNull(Locale::flagUrl('fr'));
});
test('validate locale accepts available locales', function () {
foreach (Locale::AVAILABLE_LOCALES as $locale) {
$this->assertTrue(Locale::validateLocale($locale));
}
});
test('validate locale rejects unknown locale', function () {
$this->assertFalse(Locale::validateLocale('fr'));
$this->assertFalse(Locale::validateLocale(''));
});
+95
View File
@@ -0,0 +1,95 @@
<?php
use App\Models\User;
use App\Support\Sidebar\Item;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->artisan('db:seed');
});
test('create builds item with title', function () {
$item = Item::create('Users');
$this->assertSame('Users', $item->getTitle());
});
test('href takes priority over route', function () {
$item = Item::create('Foo')
->href('https://example.com/bar');
$this->assertSame('https://example.com/bar', $item->getHref());
});
test('get href resolves named route', function () {
$item = Item::create('Foo')->route('dashboard');
$this->assertSame(route('dashboard'), $item->getHref());
});
test('get href returns null when neither set', function () {
$this->assertNull(Item::create('Foo')->getHref());
});
test('icon setter and getter', function () {
$item = Item::create('Foo')->icon('bi-people');
$this->assertSame('bi-people', $item->getIcon());
});
test('active path setter and getter', function () {
$item = Item::create('Foo')->active('users*');
$this->assertSame('users*', $item->getActivePath());
});
test('dropdown returns false without children', function () {
$this->assertFalse(Item::create('Foo')->isDropdown());
});
test('add children makes item a dropdown', function () {
$item = Item::create('Foo')->addChildren([
Item::create('Child 1'),
Item::create('Child 2'),
]);
$this->assertTrue($item->isDropdown());
$this->assertCount(2, $item->children());
});
test('expanded path collects children active paths', function () {
$item = Item::create('Foo')->addChildren([
Item::create('Child 1')->active('users*'),
Item::create('Child 2')->active('roles*'),
]);
$this->assertSame(['users*', 'roles*'], $item->getExpandedPath());
});
test('authorize returns true when no permissions set', function () {
$user = User::factory()->create();
$this->assertTrue(Item::create('Foo')->authorize($user));
});
test('authorize calls closure when closure provided', function () {
$user = User::factory()->create();
$item = Item::create('Foo')->permissions(fn ($u) => $u->id === $user->id);
$this->assertTrue($item->authorize($user));
});
test('authorize checks user has permission', function () {
$user = Mockery::mock(User::class)->makePartial();
$user->shouldReceive('hasPermission')->with('users.manage')->andReturn(true);
$user->shouldReceive('hasPermission')->with('roles.manage')->andReturn(false);
$allowed = Item::create('A')->permissions(['users.manage']);
$denied = Item::create('B')->permissions(['users.manage', 'roles.manage']);
$this->assertTrue($allowed->authorize($user));
$this->assertFalse($denied->authorize($user));
});
+28
View File
@@ -0,0 +1,28 @@
<?php
use App\View\Components\Logo;
test('default variant uses logo image', function () {
$logo = new Logo;
$this->assertStringContainsString('assets/img/logo.png', $logo->src);
});
test('no text variant uses no text image', function () {
$logo = new Logo(variant: 'no-text');
$this->assertStringContainsString('assets/img/logo-no-text.png', $logo->src);
});
test('height and class passthrough', function () {
$logo = new Logo(class: 'header-logo', height: '32');
$this->assertSame('header-logo', $logo->class);
$this->assertSame('32', $logo->height);
});
test('render returns a view', function () {
$rendered = (new Logo)->render();
$this->assertSame('components.logo', $rendered->name());
});