feat: complete Epic 1 — team management & permission system
- Story 1.1: Permission enum, config, AuthorizesPermissions & HasWorkspaceScope traits, member→worker migration - Story 1.2: Team page with member list, invitation system with queued email - Story 1.3: Role assignment (Manager/Worker) and member removal with activity logging - Story 1.4: Owner-only permission toggle matrix for Managers (manage team, view logs, configure portal) - Story 1.5: Role-based access enforcement — Workers see only assigned declarations/clients, sidebar scoping - Story 1.6: Workspace switcher dropdown for multi-workspace users with session-based switching - 83 new/modified files, 182 tests passing with zero regressions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
214
tests/Feature/Team/ManageTeamTest.php
Normal file
214
tests/Feature/Team/ManageTeamTest.php
Normal file
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Mail\TeamInvitationMail;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
function setupTeamUser(string $role, array $permissions = []): array
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user->id, [
|
||||
'role' => $role,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
return [$user, $workspace];
|
||||
}
|
||||
|
||||
// ── Team Index ──────────────────────────────────────────────
|
||||
|
||||
test('owner can view team index page', function () {
|
||||
[$user, $workspace] = setupTeamUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('team.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('team/Index')
|
||||
->has('members')
|
||||
->has('pendingInvitations')
|
||||
->where('canManageTeam', true)
|
||||
->has('inviteUrl')
|
||||
->has('roles')
|
||||
);
|
||||
});
|
||||
|
||||
test('manager with can_manage_team can view team index page', function () {
|
||||
[$user] = setupTeamUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('team.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('team/Index')
|
||||
->where('canManageTeam', true)
|
||||
);
|
||||
});
|
||||
|
||||
test('manager without can_manage_team can view team index but canManageTeam is false', function () {
|
||||
[$user] = setupTeamUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('team.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('team/Index')
|
||||
->where('canManageTeam', false)
|
||||
);
|
||||
});
|
||||
|
||||
test('worker receives 404 on team index', function () {
|
||||
[$user] = setupTeamUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('team.index'));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Invite Member ───────────────────────────────────────────
|
||||
|
||||
test('owner can invite a new member', function () {
|
||||
Mail::fake();
|
||||
[$user, $workspace] = setupTeamUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'newmember@example.com',
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Invitation envoyée');
|
||||
|
||||
expect(TeamInvitation::where('workspace_id', $workspace->id)
|
||||
->where('email', 'newmember@example.com')
|
||||
->exists()
|
||||
)->toBeTrue();
|
||||
|
||||
Mail::assertQueued(TeamInvitationMail::class, function ($mail) {
|
||||
return $mail->hasTo('newmember@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
test('manager with can_manage_team can invite a new member', function () {
|
||||
Mail::fake();
|
||||
[$user, $workspace] = setupTeamUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'newmanager@example.com',
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Invitation envoyée');
|
||||
|
||||
expect(TeamInvitation::where('workspace_id', $workspace->id)
|
||||
->where('email', 'newmanager@example.com')
|
||||
->where('role', 'manager')
|
||||
->exists()
|
||||
)->toBeTrue();
|
||||
});
|
||||
|
||||
test('manager without permission gets 404 on invite', function () {
|
||||
[$user] = setupTeamUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'blocked@example.com',
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
test('worker gets 404 on invite', function () {
|
||||
[$user] = setupTeamUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'blocked@example.com',
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
test('cannot invite email already in workspace', function () {
|
||||
[$user, $workspace] = setupTeamUser(WorkspaceUserRole::Owner);
|
||||
$existingMember = User::factory()->create(['email' => 'existing@example.com']);
|
||||
$workspace->users()->attach($existingMember->id, ['role' => 'worker']);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'existing@example.com',
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
|
||||
test('invitation creates team invitation record with correct data', function () {
|
||||
Mail::fake();
|
||||
[$user, $workspace] = setupTeamUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'record@example.com',
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$invitation = TeamInvitation::where('email', 'record@example.com')->first();
|
||||
|
||||
expect($invitation)->not->toBeNull()
|
||||
->and($invitation->workspace_id)->toBe($workspace->id)
|
||||
->and($invitation->role)->toBe('manager')
|
||||
->and($invitation->invited_by)->toBe($user->id)
|
||||
->and($invitation->token)->not->toBeEmpty()
|
||||
->and($invitation->expires_at)->not->toBeNull()
|
||||
->and($invitation->accepted_at)->toBeNull();
|
||||
});
|
||||
|
||||
test('invitation sends email', function () {
|
||||
Mail::fake();
|
||||
[$user] = setupTeamUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'mailto@example.com',
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
Mail::assertQueued(TeamInvitationMail::class, function ($mail) {
|
||||
return $mail->hasTo('mailto@example.com');
|
||||
});
|
||||
});
|
||||
|
||||
test('cannot send duplicate active invitation to same email', function () {
|
||||
Mail::fake();
|
||||
[$user, $workspace] = setupTeamUser(WorkspaceUserRole::Owner);
|
||||
|
||||
// Create an existing active invitation
|
||||
TeamInvitation::create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'email' => 'duplicate@example.com',
|
||||
'role' => 'worker',
|
||||
'invited_by' => $user->id,
|
||||
'expires_at' => now()->addDays(7),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('team.invite'), [
|
||||
'email' => 'duplicate@example.com',
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
});
|
||||
Reference in New Issue
Block a user