Invitation system test
Some checks failed
linter / quality (push) Successful in 4m37s
tests / ci (push) Failing after 8m58s

This commit is contained in:
Javier Feliz 2025-08-02 14:11:43 -04:00
parent c976cf6e53
commit 292ec10b48
5 changed files with 295 additions and 11 deletions

View File

@ -30,6 +30,10 @@ class ManageUsers extends Component
{
$this->authorize('invite', User::class);
$this->validate([
'invite_email' => 'required|email|unique:invitations,email|unique:users,email',
]);
$inv = Invitation::create([
'code' => str()->random(50),
'email' => $this->invite_email,
@ -38,7 +42,8 @@ class ManageUsers extends Component
]);
// Send email if checkbox is checked
if ($this->send_email) {
$emailSent = $this->send_email;
if ($emailSent) {
Mail::to($inv->email)->send(new InvitationMail($inv));
}
@ -47,10 +52,9 @@ class ManageUsers extends Component
// Refresh the data
$this->invitations->prepend($inv);
$this->reset(['invite_email', 'send_email']);
$this->invite_email = '';
$this->send_email = false;
session()->flash('success', 'Invitation created successfully' . ($this->send_email ? ' and email sent' : '') . '.');
$message = 'Invitation created successfully' . ($emailSent ? ' and email sent' : '') . '.';
session()->flash('success', $message);
}
public function deleteUser(User $user)

View File

@ -2,11 +2,14 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Invitation extends Model
{
use HasFactory;
protected $guarded = ['id'];
protected $casts = [
'expires_at' => 'datetime',

View File

@ -19,7 +19,7 @@ class InvitationFactory extends Factory
public function definition(): array
{
return [
'code' => Invitation::generateCode(),
'code' => fake()->unique()->regexify('[A-Za-z0-9]{50}'),
'email' => fake()->unique()->safeEmail(),
'invited_by' => User::factory(),
'expires_at' => now()->addDays(7),

View File

@ -7,9 +7,3 @@ uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
test('guests are redirected to the login page', function () {
$this->get('/dashboard')->assertRedirect('/login');
});
test('authenticated users can visit the dashboard', function () {
$this->actingAs($user = User::factory()->create());
$this->get('/dashboard')->assertStatus(200);
});

View File

@ -0,0 +1,283 @@
<?php
use App\Livewire\ManageUsers;
use App\Mail\InvitationMail;
use App\Models\Invitation;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create test users
$this->admin = User::factory()->create(['is_admin' => true]);
$this->user = User::factory()->create(['is_admin' => false]);
});
describe('ManageUsers Component', function () {
describe('invitation creation', function () {
it('allows admins to create invitations', function () {
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->set('invite_email', 'test@example.com')
->call('inviteUser')
->assertHasNoErrors();
$this->assertDatabaseHas('invitations', [
'email' => 'test@example.com',
'invited_by' => $this->admin->id,
]);
});
it('prevents non-admins from creating invitations', function () {
// Test authorization at the method level without rendering view
$this->actingAs($this->user);
$component = new ManageUsers();
$component->invite_email = 'test@example.com';
$this->expectException(\Illuminate\Auth\Access\AuthorizationException::class);
$component->inviteUser();
});
it('validates email format when creating invitations', function () {
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->set('invite_email', 'invalid-email')
->call('inviteUser')
->assertHasErrors('invite_email');
});
it('generates a random code and sets expiration when creating invitations', function () {
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->set('invite_email', 'test@example.com')
->call('inviteUser');
$invitation = Invitation::where('email', 'test@example.com')->first();
expect($invitation->code)->toHaveLength(50);
expect($invitation->expires_at)->not()->toBeNull();
expect($invitation->invited_by)->toBe($this->admin->id);
});
});
describe('email sending', function () {
it('sends email when checkbox is checked', function () {
Mail::fake();
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->set('invite_email', 'test@example.com')
->set('send_email', true)
->call('inviteUser');
$invitation = Invitation::where('email', 'test@example.com')->first();
Mail::assertSent(InvitationMail::class, function ($mail) use ($invitation) {
return $mail->hasTo('test@example.com') &&
$mail->invitation->id === $invitation->id;
});
});
it('does not send email when checkbox is unchecked', function () {
Mail::fake();
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->set('invite_email', 'test@example.com')
->set('send_email', false)
->call('inviteUser');
Mail::assertNotSent(InvitationMail::class);
});
it('sends email to the correct recipient', function () {
Mail::fake();
$this->actingAs($this->admin);
$testEmail = 'specific@example.com';
Livewire::test(ManageUsers::class)
->set('invite_email', $testEmail)
->set('send_email', true)
->call('inviteUser');
Mail::assertSent(InvitationMail::class, function ($mail) use ($testEmail) {
return $mail->hasTo($testEmail);
});
Mail::assertNotSent(InvitationMail::class, function ($mail) {
return $mail->hasTo('wrong@example.com');
});
});
});
describe('invitation deletion', function () {
it('allows admins to delete pending invitations', function () {
$this->actingAs($this->admin);
$invitation = Invitation::factory()->create([
'accepted_at' => null, // Pending invitation
]);
Livewire::test(ManageUsers::class)
->call('deleteInvitation', $invitation)
->assertHasNoErrors();
$this->assertDatabaseMissing('invitations', [
'id' => $invitation->id,
]);
});
it('prevents non-admins from deleting invitations', function () {
// Test authorization at the method level without rendering view
$this->actingAs($this->user);
$invitation = Invitation::factory()->create([
'accepted_at' => null,
]);
$component = new ManageUsers();
$this->expectException(\Illuminate\Auth\Access\AuthorizationException::class);
$component->deleteInvitation($invitation);
});
it('prevents deletion of accepted invitations', function () {
$this->actingAs($this->admin);
$invitation = Invitation::factory()->create([
'accepted_at' => now(), // Accepted invitation
]);
Livewire::test(ManageUsers::class)
->call('deleteInvitation', $invitation)
->assertHasNoErrors(); // Component handles this gracefully
// Invitation should still exist
$this->assertDatabaseHas('invitations', [
'id' => $invitation->id,
]);
});
});
describe('component state management', function () {
it('resets form fields after successful invitation creation', function () {
$this->actingAs($this->admin);
$component = Livewire::test(ManageUsers::class)
->set('invite_email', 'test@example.com')
->set('send_email', true)
->call('inviteUser');
expect($component->get('invite_email'))->toBe('');
expect($component->get('send_email'))->toBe(false);
});
it('updates invitations collection after creation', function () {
$this->actingAs($this->admin);
$component = Livewire::test(ManageUsers::class);
$initialCount = $component->get('invitations')->count();
$component
->set('invite_email', 'test@example.com')
->call('inviteUser');
expect($component->get('invitations')->count())->toBe($initialCount + 1);
});
it('shows success message after invitation creation', function () {
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->set('invite_email', 'test@example.com')
->set('send_email', false)
->call('inviteUser')
->assertHasNoErrors();
// Verify the invitation was created
$this->assertDatabaseHas('invitations', [
'email' => 'test@example.com',
]);
});
it('tracks email sending status correctly', function () {
Mail::fake();
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->set('invite_email', 'test@example.com')
->set('send_email', true)
->call('inviteUser')
->assertHasNoErrors();
// Verify the invitation was created
$this->assertDatabaseHas('invitations', [
'email' => 'test@example.com',
]);
// Verify email was sent
Mail::assertSent(InvitationMail::class);
});
});
describe('authorization integration', function () {
it('loads data for admin users', function () {
$this->actingAs($this->admin);
$adminComponent = Livewire::test(ManageUsers::class);
expect($adminComponent->get('users'))->not()->toBeNull();
expect($adminComponent->get('invitations'))->not()->toBeNull();
});
});
describe('user role management', function () {
it('allows admins to change user roles', function () {
$this->actingAs($this->admin);
$targetUser = User::factory()->create(['is_admin' => false]);
Livewire::test(ManageUsers::class)
->call('changeUserRole', $targetUser, 'admin')
->assertHasNoErrors();
expect($targetUser->fresh()->is_admin)->toBe(true);
});
it('prevents admins from demoting themselves', function () {
$this->actingAs($this->admin);
Livewire::test(ManageUsers::class)
->call('changeUserRole', $this->admin, 'user')
->assertHasNoErrors();
// Verify the admin status wasn't changed
expect($this->admin->fresh()->is_admin)->toBe(true);
});
it('allows admins to delete other users', function () {
$this->actingAs($this->admin);
$targetUser = User::factory()->create();
Livewire::test(ManageUsers::class)
->call('deleteUser', $targetUser)
->assertHasNoErrors();
$this->assertDatabaseMissing('users', [
'id' => $targetUser->id,
]);
});
});
});