From a3e34886e4bd08392b2cae3b38de5000de2bc641 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 20 May 2026 06:21:53 +0000 Subject: [PATCH] Add initial Pest unit and feature test suite Configure in-memory SQLite and APP_KEY for the test environment. Add shared Pest helpers for seeding roles and Filament authentication. Cover roles enum, user model behavior, user observer, invite mail, welcome route, dashboard access, and Filament user list/invite flows. Closes #5 Co-authored-by: Theron Smith --- phpunit.xml | 5 +- tests/Feature/ExampleTest.php | 7 -- .../Filament/Auth/DashboardAccessTest.php | 18 ++++ .../Filament/Users/InviteUserActionTest.php | 58 +++++++++++++ .../Feature/Filament/Users/ListUsersTest.php | 34 ++++++++ tests/Feature/Http/WelcomePageTest.php | 7 ++ tests/Feature/Mail/InviteUserTest.php | 35 ++++++++ tests/Feature/Models/UserTest.php | 82 +++++++++++++++++++ tests/Feature/Observers/UserObserverTest.php | 30 +++++++ tests/Pest.php | 54 ++++++------ tests/Unit/Enums/RolesEnumTest.php | 28 +++++++ tests/Unit/ExampleTest.php | 5 -- 12 files changed, 326 insertions(+), 37 deletions(-) delete mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Feature/Filament/Auth/DashboardAccessTest.php create mode 100644 tests/Feature/Filament/Users/InviteUserActionTest.php create mode 100644 tests/Feature/Filament/Users/ListUsersTest.php create mode 100644 tests/Feature/Http/WelcomePageTest.php create mode 100644 tests/Feature/Mail/InviteUserTest.php create mode 100644 tests/Feature/Models/UserTest.php create mode 100644 tests/Feature/Observers/UserObserverTest.php create mode 100644 tests/Unit/Enums/RolesEnumTest.php delete mode 100644 tests/Unit/ExampleTest.php 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(); -});