diff --git a/phpunit.xml b/phpunit.xml index 506b9a3..7bd9467 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,9 +21,10 @@ + - - + + diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 8b5843f..0000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,7 +0,0 @@ -get('/'); - - $response->assertStatus(200); -}); diff --git a/tests/Feature/Filament/Auth/DashboardAccessTest.php b/tests/Feature/Filament/Auth/DashboardAccessTest.php new file mode 100644 index 0000000..6a3a476 --- /dev/null +++ b/tests/Feature/Filament/Auth/DashboardAccessTest.php @@ -0,0 +1,18 @@ +get('/dashboard') + ->assertRedirect(); + }); + + it('allows authenticated users to access the dashboard', function () { + $user = createUserWithRole(RolesEnum::EDITOR); + + $this->actingAs($user) + ->get('/dashboard') + ->assertSuccessful(); + }); +}); diff --git a/tests/Feature/Filament/Users/InviteUserActionTest.php b/tests/Feature/Filament/Users/InviteUserActionTest.php new file mode 100644 index 0000000..19c0860 --- /dev/null +++ b/tests/Feature/Filament/Users/InviteUserActionTest.php @@ -0,0 +1,58 @@ + 'Invited User', + 'email' => 'invited@example.com', + 'role' => RolesEnum::EDITOR->value, + ]); + + $user = User::query()->where('email', 'invited@example.com')->first(); + + expect($user)->not->toBeNull() + ->and($user->name)->toBe('Invited User') + ->and($user->password)->toBeNull() + ->and($user->invite_token)->not->toBeEmpty() + ->and($user->hasRole(RolesEnum::EDITOR))->toBeTrue(); + + Mail::assertSent(InviteUser::class, function (InviteUser $mail) use ($user) { + return $mail->hasTo('invited@example.com') + && $mail->user->is($user); + }); + }); + + it('can invite a user from the users list page', function () { + $admin = createUserWithRole(RolesEnum::ADMIN); + + actingAsFilamentUser($admin); + + Livewire::test(ListUsers::class) + ->callAction('invite-user', data: [ + 'name' => 'Filament Invitee', + 'email' => 'filament-invitee@example.com', + 'role' => RolesEnum::ADMIN->value, + ]) + ->assertNotified(); + + $user = User::query()->where('email', 'filament-invitee@example.com')->first(); + + expect($user)->not->toBeNull() + ->and($user->hasRole(RolesEnum::ADMIN))->toBeTrue(); + + Mail::assertSent(InviteUser::class); + }); +}); diff --git a/tests/Feature/Filament/Users/ListUsersTest.php b/tests/Feature/Filament/Users/ListUsersTest.php new file mode 100644 index 0000000..0b7ee48 --- /dev/null +++ b/tests/Feature/Filament/Users/ListUsersTest.php @@ -0,0 +1,34 @@ +count(2)->create(); + + actingAsFilamentUser($admin); + + Livewire::test(ListUsers::class) + ->assertSuccessful() + ->loadTable() + ->assertCanSeeTableRecords($users); + }); + + it('can search users by name', function () { + $admin = createUserWithRole(RolesEnum::ADMIN); + $matchingUser = User::factory()->create(['name' => 'Unique Search Name']); + $otherUser = User::factory()->create(['name' => 'Someone Else']); + + actingAsFilamentUser($admin); + + Livewire::test(ListUsers::class) + ->loadTable() + ->searchTable('Unique Search Name') + ->assertCanSeeTableRecords([$matchingUser]) + ->assertCanNotSeeTableRecords([$otherUser]); + }); +}); diff --git a/tests/Feature/Http/WelcomePageTest.php b/tests/Feature/Http/WelcomePageTest.php new file mode 100644 index 0000000..354587d --- /dev/null +++ b/tests/Feature/Http/WelcomePageTest.php @@ -0,0 +1,7 @@ +get('/') + ->assertSuccessful() + ->assertViewIs('welcome'); +}); diff --git a/tests/Feature/Mail/InviteUserTest.php b/tests/Feature/Mail/InviteUserTest.php new file mode 100644 index 0000000..d91ab60 --- /dev/null +++ b/tests/Feature/Mail/InviteUserTest.php @@ -0,0 +1,35 @@ +make([ + 'invite_token' => Str::random(60), + ]); + + expect((new InviteUser($user))->envelope()->subject) + ->toBe('You have been invited to join the team!'); + }); + + it('includes a signed registration url with the invite token', function () { + $user = User::factory()->create([ + 'invite_token' => 'test-invite-token', + ]); + + $mailable = new InviteUser($user); + $content = $mailable->content(); + + expect($content->markdown)->toBe('mail.auth.invite-user') + ->and($content->with['acceptUrl']) + ->toContain('invite/register') + ->toContain('token=test-invite-token') + ->toContain('signature='); + }); +}); diff --git a/tests/Feature/Models/UserTest.php b/tests/Feature/Models/UserTest.php new file mode 100644 index 0000000..4c4091b --- /dev/null +++ b/tests/Feature/Models/UserTest.php @@ -0,0 +1,82 @@ +create(); + + expect($user->canAccessPanel(Filament::getPanel('dashboard')))->toBeTrue(); + }); + + it('uses the user email as the app authentication holder name', function () { + $user = User::factory()->create([ + 'email' => 'jane@example.com', + ]); + + expect($user->getAppAuthenticationHolderName())->toBe('jane@example.com'); + }); + + it('generates a ui-avatars url for the filament avatar', function () { + $user = User::factory()->create([ + 'name' => 'Jane Doe', + ]); + + expect($user->getFilamentAvatarUrl()) + ->toStartWith('https://ui-avatars.com/api/') + ->toContain('name='); + }); + + it('allows super admins to impersonate other users', function () { + $superAdmin = createUserWithRole(RolesEnum::SUPER_ADMIN); + + expect($superAdmin->canImpersonate())->toBeTrue(); + }); + + it('allows users with impersonate permission to impersonate', function () { + $admin = createUserWithRole(RolesEnum::ADMIN); + + expect($admin->canImpersonate())->toBeTrue(); + }); + + it('does not allow editors without impersonate permission to impersonate', function () { + $editor = createUserWithRole(RolesEnum::EDITOR); + + expect($editor->canImpersonate())->toBeFalse(); + }); + + it('does not allow super admins to be impersonated', function () { + $superAdmin = createUserWithRole(RolesEnum::SUPER_ADMIN); + + expect($superAdmin->canBeImpersonated())->toBeFalse(); + }); + + it('allows non-super-admin users to be impersonated', function () { + $admin = createUserWithRole(RolesEnum::ADMIN); + + expect($admin->canBeImpersonated())->toBeTrue(); + }); + + it('persists app authentication secrets', function () { + $user = User::factory()->create(); + + $user->saveAppAuthenticationSecret('test-secret'); + + expect($user->fresh()->getAppAuthenticationSecret())->toBe('test-secret'); + }); + + it('persists app authentication recovery codes', function () { + $user = User::factory()->create(); + $codes = ['code-one', 'code-two']; + + $user->saveAppAuthenticationRecoveryCodes($codes); + + expect($user->fresh()->getAppAuthenticationRecoveryCodes())->toBe($codes); + }); +}); diff --git a/tests/Feature/Observers/UserObserverTest.php b/tests/Feature/Observers/UserObserverTest.php new file mode 100644 index 0000000..fe736a0 --- /dev/null +++ b/tests/Feature/Observers/UserObserverTest.php @@ -0,0 +1,30 @@ +create(); + + expect($user->hasRole(RolesEnum::EDITOR))->toBeTrue(); + }); + + it('does not assign the editor role when the user already has a role', function () { + $user = User::withoutEvents(function () { + $user = User::factory()->create(); + $user->assignRole(RolesEnum::ADMIN); + + return $user; + }); + + (new \App\Observers\UserObserver)->created($user); + + expect($user->hasRole(RolesEnum::ADMIN))->toBeTrue() + ->and($user->hasRole(RolesEnum::EDITOR))->toBeFalse(); + }); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 60f04a4..af3af63 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1,29 +1,22 @@ extend(Tests\TestCase::class) - // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + ->in('Unit'); + +pest()->extend(Tests\TestCase::class) + ->use(RefreshDatabase::class) ->in('Feature'); /* |-------------------------------------------------------------------------- | Expectations |-------------------------------------------------------------------------- -| -| When you're writing tests, you often need to check that values meet certain conditions. The -| "expect()" function gives you access to a set of "expectations" methods that you can use -| to assert different things. Of course, you may extend the Expectation API at any time. -| */ expect()->extend('toBeOne', function () { @@ -34,14 +27,29 @@ |-------------------------------------------------------------------------- | Functions |-------------------------------------------------------------------------- -| -| While Pest is very powerful out-of-the-box, you may have some testing code specific to your -| project that you don't want to repeat in every file. Here you can also expose helpers as -| global functions to help you to reduce the number of lines of code in your test files. -| */ -function something() +function seedRolesAndPermissions(): void +{ + test()->seed(RolesAndPermissionsSeeder::class); +} + +function createUserWithRole(RolesEnum $role, array $attributes = []): User { - // .. + seedRolesAndPermissions(); + + $user = User::factory()->create($attributes); + + $user->assignRole($role); + + return $user; +} + +function actingAsFilamentUser(User $user): User +{ + test()->actingAs($user); + + Filament::setCurrentPanel(Filament::getPanel('dashboard')); + + return $user; } diff --git a/tests/Unit/Enums/RolesEnumTest.php b/tests/Unit/Enums/RolesEnumTest.php new file mode 100644 index 0000000..b908504 --- /dev/null +++ b/tests/Unit/Enums/RolesEnumTest.php @@ -0,0 +1,28 @@ +toHaveCount(3) + ->and(RolesEnum::SUPER_ADMIN->value)->toBe('super-admin') + ->and(RolesEnum::ADMIN->value)->toBe('admin') + ->and(RolesEnum::EDITOR->value)->toBe('editor'); + }); + + it('returns human-readable labels', function (RolesEnum $role, string $label) { + expect($role->getLabel())->toBe($label); + })->with([ + 'super admin' => [RolesEnum::SUPER_ADMIN, 'Super Admin'], + 'admin' => [RolesEnum::ADMIN, 'Admin'], + 'editor' => [RolesEnum::EDITOR, 'Editor'], + ]); + + it('returns filament colors', function (RolesEnum $role, string $color) { + expect($role->getColor())->toBe($color); + })->with([ + 'super admin' => [RolesEnum::SUPER_ADMIN, 'primary'], + 'admin' => [RolesEnum::ADMIN, 'success'], + 'editor' => [RolesEnum::EDITOR, 'warning'], + ]); +}); diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index 44a4f33..0000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -});