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:
245
tests/Feature/Client/RoleBasedAccessTest.php
Normal file
245
tests/Feature/Client/RoleBasedAccessTest.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\Client;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
function setupClientTestUser(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];
|
||||
}
|
||||
|
||||
// AC #1: Workers see only clients with assigned declarations
|
||||
test('worker sees only clients with assigned declarations in index', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$clientWithAssignment = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$clientWithoutAssignment = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $clientWithAssignment->id,
|
||||
'assigned_to' => $worker->id,
|
||||
]);
|
||||
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $clientWithoutAssignment->id,
|
||||
'assigned_to' => null,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('clients.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('clients/Index')
|
||||
->has('clients.data', 1)
|
||||
->where('clients.data.0.id', $clientWithAssignment->id)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #3: Owners see all workspace clients
|
||||
test('owner sees all workspace clients in index', function () {
|
||||
[$owner, $workspace] = setupClientTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
Client::factory()->count(3)->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('clients.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('clients/Index')
|
||||
->has('clients.data', 3)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #3: Managers see all workspace clients
|
||||
test('manager sees all workspace clients in index', function () {
|
||||
[$manager, $workspace] = setupClientTestUser(WorkspaceUserRole::Manager);
|
||||
|
||||
Client::factory()->count(3)->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($manager)->get(route('clients.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('clients/Index')
|
||||
->has('clients.data', 3)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #5: Worker gets 404 accessing client with no assigned declarations
|
||||
test('worker gets 404 accessing client with no assigned declarations', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
// No declarations assigned to this worker for this client
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => null,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('clients.show', $client));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Worker can access client show when they have assigned declarations
|
||||
test('worker can access client show when they have assigned declarations', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $worker->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('clients.show', $client));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('clients/Show')
|
||||
);
|
||||
});
|
||||
|
||||
// AC #6: Worker gets 404 on create
|
||||
test('worker gets 404 on client create', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('clients.create'));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// AC #6: Worker gets 404 on store
|
||||
test('worker gets 404 on client store', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($worker)->post(route('clients.store'), [
|
||||
'company_name' => 'Test',
|
||||
'legal_form' => 'sarl',
|
||||
'contacts' => [['full_name' => 'Test', 'is_principal' => true]],
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// AC #6: Worker gets 404 on edit
|
||||
test('worker gets 404 on client edit', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('clients.edit', $client));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// AC #6: Worker gets 404 on update
|
||||
test('worker gets 404 on client update', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($worker)->put(route('clients.update', $client), [
|
||||
'company_name' => 'Updated',
|
||||
'legal_form' => 'sarl',
|
||||
'contacts' => [
|
||||
['full_name' => 'Test Contact', 'is_principal' => true],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// AC #6: Worker gets 404 on destroy
|
||||
test('worker gets 404 on client destroy', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($worker)->delete(route('clients.destroy', $client));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// AC #3: Manager can access all CRUD operations
|
||||
test('manager can access all client CRUD operations', function () {
|
||||
[$manager, $workspace] = setupClientTestUser(WorkspaceUserRole::Manager);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
// Index
|
||||
$this->actingAs($manager)->get(route('clients.index'))->assertOk();
|
||||
|
||||
// Show
|
||||
$this->actingAs($manager)->get(route('clients.show', $client))->assertOk();
|
||||
|
||||
// Create page
|
||||
$this->actingAs($manager)->get(route('clients.create'))->assertOk();
|
||||
|
||||
// Edit page
|
||||
$this->actingAs($manager)->get(route('clients.edit', $client))->assertOk();
|
||||
});
|
||||
|
||||
// AC #3: Owner can access all CRUD operations
|
||||
test('owner can access all client CRUD operations', function () {
|
||||
[$owner, $workspace] = setupClientTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
// Index
|
||||
$this->actingAs($owner)->get(route('clients.index'))->assertOk();
|
||||
|
||||
// Show
|
||||
$this->actingAs($owner)->get(route('clients.show', $client))->assertOk();
|
||||
|
||||
// Create page
|
||||
$this->actingAs($owner)->get(route('clients.create'))->assertOk();
|
||||
|
||||
// Edit page
|
||||
$this->actingAs($owner)->get(route('clients.edit', $client))->assertOk();
|
||||
});
|
||||
|
||||
// AC #10: canCreate/canEdit/canDelete props are false for Workers
|
||||
test('worker gets canCreate canEdit canDelete as false in index', function () {
|
||||
[$worker, $workspace] = setupClientTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('clients.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('canCreate', false)
|
||||
->where('canEdit', false)
|
||||
->where('canDelete', false)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #10: canCreate/canEdit/canDelete props are true for Owners
|
||||
test('owner gets canCreate canEdit canDelete as true in index', function () {
|
||||
[$owner, $workspace] = setupClientTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('clients.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('canCreate', true)
|
||||
->where('canEdit', true)
|
||||
->where('canDelete', true)
|
||||
);
|
||||
});
|
||||
64
tests/Feature/Database/MemberToWorkerMigrationTest.php
Normal file
64
tests/Feature/Database/MemberToWorkerMigrationTest.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
function getMigrationInstance(): object
|
||||
{
|
||||
return require database_path('migrations/2026_03_14_000001_rename_member_to_worker_in_workspace_user.php');
|
||||
}
|
||||
|
||||
test('data migration renames member to worker in workspace_user role', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
// Insert a record with the old 'member' value directly via DB
|
||||
DB::table('workspace_user')->insert([
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $user->id,
|
||||
'role' => 'member',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
// Verify it has 'member' role
|
||||
expect(
|
||||
DB::table('workspace_user')
|
||||
->where('user_id', $user->id)
|
||||
->where('workspace_id', $workspace->id)
|
||||
->value('role')
|
||||
)->toBe('member');
|
||||
|
||||
// Run the actual migration file
|
||||
getMigrationInstance()->up();
|
||||
|
||||
// Verify it now has 'worker' role
|
||||
expect(
|
||||
DB::table('workspace_user')
|
||||
->where('user_id', $user->id)
|
||||
->where('workspace_id', $workspace->id)
|
||||
->value('role')
|
||||
)->toBe('worker');
|
||||
});
|
||||
|
||||
test('data migration does not affect owner or manager roles', function () {
|
||||
$owner = User::factory()->create();
|
||||
$manager = User::factory()->create();
|
||||
$worker = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
|
||||
DB::table('workspace_user')->insert([
|
||||
['workspace_id' => $workspace->id, 'user_id' => $owner->id, 'role' => 'owner', 'created_at' => now(), 'updated_at' => now()],
|
||||
['workspace_id' => $workspace->id, 'user_id' => $manager->id, 'role' => 'manager', 'created_at' => now(), 'updated_at' => now()],
|
||||
['workspace_id' => $workspace->id, 'user_id' => $worker->id, 'role' => 'member', 'created_at' => now(), 'updated_at' => now()],
|
||||
]);
|
||||
|
||||
// Run the actual migration file
|
||||
getMigrationInstance()->up();
|
||||
|
||||
// Owner and manager should remain unchanged
|
||||
expect(DB::table('workspace_user')->where('user_id', $owner->id)->value('role'))->toBe('owner');
|
||||
expect(DB::table('workspace_user')->where('user_id', $manager->id)->value('role'))->toBe('manager');
|
||||
expect(DB::table('workspace_user')->where('user_id', $worker->id)->value('role'))->toBe('worker');
|
||||
});
|
||||
@@ -36,8 +36,8 @@ test('composite index exists on messages declaration_id and created_at', functio
|
||||
});
|
||||
|
||||
test('migration is reversible and rollback restores folder tables', function () {
|
||||
// Rollback foundation migrations (3) + polymorphic update + rename migration
|
||||
$this->artisan('migrate:rollback', ['--step' => 5]);
|
||||
// Rollback team_invitations (1) + member-to-worker rename (1) + foundation migrations (3) + polymorphic update + rename migration
|
||||
$this->artisan('migrate:rollback', ['--step' => 7]);
|
||||
|
||||
expect(Schema::hasTable('folders'))->toBeTrue();
|
||||
expect(Schema::hasTable('declarations'))->toBeFalse();
|
||||
|
||||
@@ -80,7 +80,7 @@ test('re-downloading updates timestamp without creating duplicates', function ()
|
||||
test('download status is per-user in show endpoint', function () {
|
||||
[$user, $workspace, $declaration, $media] = setupDeclarationWithMedia();
|
||||
$otherUser = User::factory()->create();
|
||||
$workspace->users()->attach($otherUser, ['role' => 'member']);
|
||||
$workspace->users()->attach($otherUser, ['role' => 'manager']);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
MediaDownload::query()->create([
|
||||
|
||||
377
tests/Feature/Declaration/RoleBasedAccessTest.php
Normal file
377
tests/Feature/Declaration/RoleBasedAccessTest.php
Normal file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\Client;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
|
||||
function setupDeclarationTestUser(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];
|
||||
}
|
||||
|
||||
// AC #2: Worker sees only assigned declarations in index
|
||||
test('worker sees only assigned declarations in index', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$assignedDeclaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $worker->id,
|
||||
]);
|
||||
|
||||
$unassignedDeclaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => null,
|
||||
]);
|
||||
|
||||
$otherUser = User::factory()->create();
|
||||
$otherAssignedDeclaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $otherUser->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('declarations/Index')
|
||||
->has('declarations.data', 1)
|
||||
->where('declarations.data.0.id', $assignedDeclaration->id)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #3: Owner sees all declarations
|
||||
test('owner sees all declarations in index', function () {
|
||||
[$owner, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
Declaration::factory()->count(5)->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('declarations.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('declarations/Index')
|
||||
->has('declarations.data', 5)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #3: Manager sees all declarations
|
||||
test('manager sees all declarations in index', function () {
|
||||
[$manager, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Manager);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
Declaration::factory()->count(3)->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($manager)->get(route('declarations.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('declarations/Index')
|
||||
->has('declarations.data', 3)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #4: Worker gets 404 accessing unassigned declaration via direct URL
|
||||
test('worker gets 404 accessing unassigned declaration via direct URL', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => null,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.show', $declaration));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Worker can access assigned declaration show
|
||||
test('worker can access assigned declaration show', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $worker->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.show', $declaration));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('declarations/Show')
|
||||
);
|
||||
});
|
||||
|
||||
// AC #6: Worker gets 404 on create
|
||||
test('worker gets 404 on declaration create', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.create'));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Worker gets 404 on store
|
||||
test('worker gets 404 on declaration store', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($worker)->post(route('declarations.store'), [
|
||||
'title' => 'Test',
|
||||
'type' => 'vat_monthly',
|
||||
'client_id' => $client->id,
|
||||
'period_year' => 2026,
|
||||
'period_month' => 1,
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Worker gets 404 on edit
|
||||
test('worker gets 404 on declaration edit', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.edit', $declaration));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Worker gets 404 on update
|
||||
test('worker gets 404 on declaration update', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->put(route('declarations.update', $declaration), [
|
||||
'title' => 'Updated',
|
||||
'type' => 'vat_monthly',
|
||||
'client_id' => $client->id,
|
||||
'period_year' => 2026,
|
||||
'period_month' => 1,
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Worker gets 404 on destroy
|
||||
test('worker gets 404 on declaration destroy', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->delete(route('declarations.destroy', $declaration));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// Manager can access all CRUD operations
|
||||
test('manager can access all declaration CRUD operations', function () {
|
||||
[$manager, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Manager);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
// Index
|
||||
$this->actingAs($manager)->get(route('declarations.index'))->assertOk();
|
||||
|
||||
// Show
|
||||
$this->actingAs($manager)->get(route('declarations.show', $declaration))->assertOk();
|
||||
|
||||
// Create page
|
||||
$this->actingAs($manager)->get(route('declarations.create'))->assertOk();
|
||||
|
||||
// Edit page
|
||||
$this->actingAs($manager)->get(route('declarations.edit', $declaration))->assertOk();
|
||||
});
|
||||
|
||||
// Owner can access all CRUD operations
|
||||
test('owner can access all declaration CRUD operations', function () {
|
||||
[$owner, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
// Index
|
||||
$this->actingAs($owner)->get(route('declarations.index'))->assertOk();
|
||||
|
||||
// Show
|
||||
$this->actingAs($owner)->get(route('declarations.show', $declaration))->assertOk();
|
||||
|
||||
// Create page
|
||||
$this->actingAs($owner)->get(route('declarations.create'))->assertOk();
|
||||
|
||||
// Edit page
|
||||
$this->actingAs($owner)->get(route('declarations.edit', $declaration))->assertOk();
|
||||
});
|
||||
|
||||
// AC #10: canCreate/canEdit/canDelete props false for Workers
|
||||
test('worker gets canCreate canEdit canDelete as false in declarations index', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('canCreate', false)
|
||||
->where('canEdit', false)
|
||||
->where('canDelete', false)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #10: canCreate/canEdit/canDelete props true for Owners
|
||||
test('owner gets canCreate canEdit canDelete as true in declarations index', function () {
|
||||
[$owner, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('declarations.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('canCreate', true)
|
||||
->where('canEdit', true)
|
||||
->where('canDelete', true)
|
||||
);
|
||||
});
|
||||
|
||||
// AC #9: auth.workspaceRole is shared correctly
|
||||
test('auth.workspaceRole is shared correctly for each role', function () {
|
||||
// Test Owner
|
||||
[$owner, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('declarations.index'));
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('auth.workspaceRole', 'owner')
|
||||
);
|
||||
|
||||
// Test Manager
|
||||
[$manager, $workspace2] = setupDeclarationTestUser(WorkspaceUserRole::Manager);
|
||||
|
||||
$response = $this->actingAs($manager)->get(route('declarations.index'));
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('auth.workspaceRole', 'manager')
|
||||
);
|
||||
|
||||
// Test Worker
|
||||
[$worker, $workspace3] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.index'));
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('auth.workspaceRole', 'worker')
|
||||
);
|
||||
});
|
||||
|
||||
// AC #11: Cross-workspace isolation
|
||||
test('worker in workspace A cannot see declarations in workspace B', function () {
|
||||
[$worker, $workspaceA] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$workspaceB = Workspace::factory()->create();
|
||||
$clientB = Client::factory()->create(['workspace_id' => $workspaceB->id]);
|
||||
|
||||
$declarationInB = Declaration::factory()->create([
|
||||
'workspace_id' => $workspaceB->id,
|
||||
'client_id' => $clientB->id,
|
||||
'assigned_to' => $worker->id,
|
||||
]);
|
||||
|
||||
// Worker should not see workspace B declarations in their index
|
||||
$response = $this->actingAs($worker)->get(route('declarations.index'));
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->has('declarations.data', 0)
|
||||
);
|
||||
|
||||
// Worker should get 404 trying to access workspace B declaration directly
|
||||
$response = $this->actingAs($worker)->get(route('declarations.show', $declarationInB));
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// canMention is false for workers in declaration show
|
||||
test('worker gets canMention as false in declaration show', function () {
|
||||
[$worker, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Worker);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
'assigned_to' => $worker->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($worker)->get(route('declarations.show', $declaration));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('canMention', false)
|
||||
->where('canEdit', false)
|
||||
->where('canDelete', false)
|
||||
);
|
||||
});
|
||||
|
||||
// canMention is true for owners/managers in declaration show
|
||||
test('owner gets canMention as true in declaration show', function () {
|
||||
[$owner, $workspace] = setupDeclarationTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('declarations.show', $declaration));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->where('canMention', true)
|
||||
->where('canEdit', true)
|
||||
->where('canDelete', true)
|
||||
);
|
||||
});
|
||||
@@ -13,7 +13,7 @@ function setupMentionScenario(string $role = 'owner'): array
|
||||
$target = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($sender, ['role' => $role]);
|
||||
$workspace->users()->attach($target, ['role' => 'member']);
|
||||
$workspace->users()->attach($target, ['role' => 'worker']);
|
||||
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
$declaration = Declaration::factory()->create([
|
||||
@@ -52,9 +52,9 @@ test('manager can mention a workspace user', function () {
|
||||
Notification::assertSentTo($target, DeclarationMentionNotification::class);
|
||||
});
|
||||
|
||||
test('member cannot mention a workspace user', function () {
|
||||
test('worker cannot mention a workspace user', function () {
|
||||
Notification::fake();
|
||||
[$sender, $target, $workspace, $declaration] = setupMentionScenario('member');
|
||||
[$sender, $target, $workspace, $declaration] = setupMentionScenario('worker');
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$response = $this->actingAs($sender)->post(route('declarations.mentions.store', $declaration), [
|
||||
|
||||
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');
|
||||
});
|
||||
248
tests/Feature/Team/PermissionToggleTest.php
Normal file
248
tests/Feature/Team/PermissionToggleTest.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceUser;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
function setupPermTestUser(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];
|
||||
}
|
||||
|
||||
function createPermWorkspaceMember(Workspace $workspace, string $role, array $permissions = []): User
|
||||
{
|
||||
$member = User::factory()->create();
|
||||
$workspace->users()->attach($member->id, [
|
||||
'role' => $role,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
|
||||
return $member;
|
||||
}
|
||||
|
||||
function getPermWorkspaceUserId(Workspace $workspace, User $user): int
|
||||
{
|
||||
return WorkspaceUser::where('workspace_id', $workspace->id)
|
||||
->where('user_id', $user->id)
|
||||
->firstOrFail()
|
||||
->id;
|
||||
}
|
||||
|
||||
// ── 4.1: Owner can update Manager permissions ──
|
||||
|
||||
test('owner can update manager permissions', function () {
|
||||
[$owner, $workspace] = setupPermTestUser(WorkspaceUserRole::Owner);
|
||||
$manager = createPermWorkspaceMember($workspace, WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => false,
|
||||
]);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $manager);
|
||||
|
||||
$response = $this->actingAs($owner)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => true,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Permissions mises à jour');
|
||||
|
||||
$updatedPivot = WorkspaceUser::find($pivotId);
|
||||
expect($updatedPivot->permissions[Permission::CanManageTeam])->toBeTrue()
|
||||
->and($updatedPivot->permissions[Permission::CanViewActivityLogs])->toBeTrue()
|
||||
->and($updatedPivot->permissions[Permission::CanConfigurePortal])->toBeTrue();
|
||||
});
|
||||
|
||||
// ── 4.2: Manager cannot update permissions (even with can_manage_team) ──
|
||||
|
||||
test('manager with can_manage_team cannot update permissions', function () {
|
||||
[$manager, $workspace] = setupPermTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
$otherManager = createPermWorkspaceMember($workspace, WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
]);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $otherManager);
|
||||
|
||||
$response = $this->actingAs($manager)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── 4.3: Worker cannot access permissions endpoint ──
|
||||
|
||||
test('worker cannot access permissions endpoint', function () {
|
||||
[$worker, $workspace] = setupPermTestUser(WorkspaceUserRole::Worker);
|
||||
$manager = createPermWorkspaceMember($workspace, WorkspaceUserRole::Manager);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $manager);
|
||||
|
||||
$response = $this->actingAs($worker)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── 4.4: Cannot update Owner's permissions ──
|
||||
|
||||
test('cannot update owner permissions', function () {
|
||||
[$owner, $workspace] = setupPermTestUser(WorkspaceUserRole::Owner);
|
||||
$otherOwner = createPermWorkspaceMember($workspace, WorkspaceUserRole::Owner);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $otherOwner);
|
||||
|
||||
$response = $this->actingAs($owner)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => true,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── 4.5: Cannot update Worker's permissions ──
|
||||
|
||||
test('cannot update worker permissions', function () {
|
||||
[$owner, $workspace] = setupPermTestUser(WorkspaceUserRole::Owner);
|
||||
$worker = createPermWorkspaceMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $worker);
|
||||
|
||||
$response = $this->actingAs($owner)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => true,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── 4.6: Invalid permission key is rejected ──
|
||||
|
||||
test('invalid permission key is rejected', function () {
|
||||
[$owner, $workspace] = setupPermTestUser(WorkspaceUserRole::Owner);
|
||||
$manager = createPermWorkspaceMember($workspace, WorkspaceUserRole::Manager);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $manager);
|
||||
|
||||
$response = $this->actingAs($owner)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
'invalid_permission_key' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('permissions');
|
||||
});
|
||||
|
||||
// ── 4.7: Activity log entry created on permission change ──
|
||||
|
||||
test('activity log entry created on permission change', function () {
|
||||
[$owner, $workspace] = setupPermTestUser(WorkspaceUserRole::Owner);
|
||||
$manager = createPermWorkspaceMember($workspace, WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => false,
|
||||
]);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $manager);
|
||||
|
||||
$this->actingAs($owner)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => true,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$log = Activity::latest('id')->first();
|
||||
expect($log->description)->toBe('permissions_updated')
|
||||
->and($log->properties['target_user'])->toBe($manager->name)
|
||||
->and($log->properties['old_permissions'][Permission::CanManageTeam])->toBeFalse()
|
||||
->and($log->properties['new_permissions'][Permission::CanManageTeam])->toBeTrue();
|
||||
});
|
||||
|
||||
// ── 4.8: Toggling can_manage_team off removes invite capability on next page load ──
|
||||
|
||||
test('toggling can_manage_team off removes invite capability on next page load', function () {
|
||||
[$owner, $workspace] = setupPermTestUser(WorkspaceUserRole::Owner);
|
||||
$manager = createPermWorkspaceMember($workspace, WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => false,
|
||||
]);
|
||||
$pivotId = getPermWorkspaceUserId($workspace, $manager);
|
||||
|
||||
// Toggle can_manage_team OFF
|
||||
$this->actingAs($owner)->put(route('team.updatePermissions', $pivotId), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => false,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => false,
|
||||
],
|
||||
]);
|
||||
|
||||
// Now load team page as the manager — canManageTeam should be false
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
$response = $this->actingAs($manager)->get(route('team.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->component('team/Index')
|
||||
->where('canManageTeam', false)
|
||||
);
|
||||
});
|
||||
|
||||
// ── Cross-workspace isolation: Owner cannot update permissions in another workspace ──
|
||||
|
||||
test('owner cannot update manager permissions in another workspace', function () {
|
||||
[$owner, $workspaceA] = setupPermTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
// Create a separate workspace B with its own manager
|
||||
$workspaceB = Workspace::factory()->create();
|
||||
$managerB = User::factory()->create();
|
||||
$workspaceB->users()->attach($managerB->id, [
|
||||
'role' => WorkspaceUserRole::Manager,
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => false,
|
||||
],
|
||||
]);
|
||||
$pivotIdB = WorkspaceUser::where('workspace_id', $workspaceB->id)
|
||||
->where('user_id', $managerB->id)
|
||||
->firstOrFail()
|
||||
->id;
|
||||
|
||||
// Session is set to workspace A — attempt to update manager in workspace B
|
||||
$response = $this->actingAs($owner)->put(route('team.updatePermissions', $pivotIdB), [
|
||||
'permissions' => [
|
||||
Permission::CanManageTeam => true,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
Permission::CanConfigurePortal => true,
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
|
||||
// Verify permissions were NOT changed
|
||||
$pivot = WorkspaceUser::find($pivotIdB);
|
||||
expect($pivot->permissions[Permission::CanManageTeam])->toBeFalse();
|
||||
});
|
||||
153
tests/Feature/Team/TeamMemberRemovalTest.php
Normal file
153
tests/Feature/Team/TeamMemberRemovalTest.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceUser;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
function setupRemovalTestUser(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];
|
||||
}
|
||||
|
||||
function createRemovalTestMember(Workspace $workspace, string $role, array $permissions = []): User
|
||||
{
|
||||
$member = User::factory()->create();
|
||||
$workspace->users()->attach($member->id, [
|
||||
'role' => $role,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
|
||||
return $member;
|
||||
}
|
||||
|
||||
function getRemovalPivotId(Workspace $workspace, User $user): int
|
||||
{
|
||||
return WorkspaceUser::where('workspace_id', $workspace->id)
|
||||
->where('user_id', $user->id)
|
||||
->firstOrFail()
|
||||
->id;
|
||||
}
|
||||
|
||||
// ── Owner can remove a member ──────────────────────────────
|
||||
|
||||
test('owner can remove a member', function () {
|
||||
[$owner, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Owner);
|
||||
$worker = createRemovalTestMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getRemovalPivotId($workspace, $worker);
|
||||
|
||||
$response = $this->actingAs($owner)->delete(route('team.remove', $pivotId));
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Membre retiré');
|
||||
|
||||
// Pivot row should be deleted
|
||||
expect(WorkspaceUser::find($pivotId))->toBeNull();
|
||||
});
|
||||
|
||||
// ── Owner cannot remove themselves ─────────────────────────
|
||||
|
||||
test('owner cannot remove themselves', function () {
|
||||
[$owner, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Owner);
|
||||
$pivotId = getRemovalPivotId($workspace, $owner);
|
||||
|
||||
$response = $this->actingAs($owner)->delete(route('team.remove', $pivotId));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Manager with can_manage_team can remove worker ─────────
|
||||
|
||||
test('manager with can_manage_team can remove worker', function () {
|
||||
[$manager, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
$worker = createRemovalTestMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getRemovalPivotId($workspace, $worker);
|
||||
|
||||
$response = $this->actingAs($manager)->delete(route('team.remove', $pivotId));
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Membre retiré');
|
||||
});
|
||||
|
||||
// ── Manager with can_manage_team cannot remove owner ───────
|
||||
|
||||
test('manager with can_manage_team cannot remove owner', function () {
|
||||
[$manager, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
$owner = createRemovalTestMember($workspace, WorkspaceUserRole::Owner);
|
||||
$pivotId = getRemovalPivotId($workspace, $owner);
|
||||
|
||||
$response = $this->actingAs($manager)->delete(route('team.remove', $pivotId));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Manager without can_manage_team gets 404 ───────────────
|
||||
|
||||
test('manager without can_manage_team gets 404 on remove', function () {
|
||||
[$manager, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
]);
|
||||
$worker = createRemovalTestMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getRemovalPivotId($workspace, $worker);
|
||||
|
||||
$response = $this->actingAs($manager)->delete(route('team.remove', $pivotId));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Worker gets 404 on remove attempt ──────────────────────
|
||||
|
||||
test('worker gets 404 on remove attempt', function () {
|
||||
[$worker, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Worker);
|
||||
$otherWorker = createRemovalTestMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getRemovalPivotId($workspace, $otherWorker);
|
||||
|
||||
$response = $this->actingAs($worker)->delete(route('team.remove', $pivotId));
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Member removal is logged ───────────────────────────────
|
||||
|
||||
test('member removal is logged in activity log', function () {
|
||||
[$owner, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Owner);
|
||||
$worker = createRemovalTestMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getRemovalPivotId($workspace, $worker);
|
||||
|
||||
$this->actingAs($owner)->delete(route('team.remove', $pivotId));
|
||||
|
||||
$log = Activity::latest('id')->first();
|
||||
expect($log->description)->toBe('member_removed')
|
||||
->and($log->properties['target_user'])->toBe($worker->name)
|
||||
->and($log->properties['target_email'])->toBe($worker->email)
|
||||
->and($log->properties['role'])->toBe('worker');
|
||||
});
|
||||
|
||||
// ── Removed user account still exists ──────────────────────
|
||||
|
||||
test('removed user account still exists after removal', function () {
|
||||
[$owner, $workspace] = setupRemovalTestUser(WorkspaceUserRole::Owner);
|
||||
$worker = createRemovalTestMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getRemovalPivotId($workspace, $worker);
|
||||
|
||||
$this->actingAs($owner)->delete(route('team.remove', $pivotId));
|
||||
|
||||
// User record still exists
|
||||
expect(User::find($worker->id))->not->toBeNull();
|
||||
// But pivot row is deleted
|
||||
expect(WorkspaceUser::find($pivotId))->toBeNull();
|
||||
});
|
||||
217
tests/Feature/Team/TeamRoleAssignmentTest.php
Normal file
217
tests/Feature/Team/TeamRoleAssignmentTest.php
Normal file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceUser;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
function setupRoleTestUser(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];
|
||||
}
|
||||
|
||||
function createWorkspaceMember(Workspace $workspace, string $role, array $permissions = []): User
|
||||
{
|
||||
$member = User::factory()->create();
|
||||
$workspace->users()->attach($member->id, [
|
||||
'role' => $role,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
|
||||
return $member;
|
||||
}
|
||||
|
||||
function getWorkspaceUserId(Workspace $workspace, User $user): int
|
||||
{
|
||||
return WorkspaceUser::where('workspace_id', $workspace->id)
|
||||
->where('user_id', $user->id)
|
||||
->firstOrFail()
|
||||
->id;
|
||||
}
|
||||
|
||||
// ── Owner can change roles ──────────────────────────────────
|
||||
|
||||
test('owner can change a member role from worker to manager', function () {
|
||||
[$owner, $workspace] = setupRoleTestUser(WorkspaceUserRole::Owner);
|
||||
$worker = createWorkspaceMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getWorkspaceUserId($workspace, $worker);
|
||||
|
||||
$response = $this->actingAs($owner)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Rôle mis à jour');
|
||||
|
||||
$updatedPivot = WorkspaceUser::find($pivotId);
|
||||
expect($updatedPivot->role->is(WorkspaceUserRole::Manager))->toBeTrue();
|
||||
});
|
||||
|
||||
test('owner can change a member role from manager to worker', function () {
|
||||
[$owner, $workspace] = setupRoleTestUser(WorkspaceUserRole::Owner);
|
||||
$manager = createWorkspaceMember($workspace, WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
Permission::CanViewActivityLogs => true,
|
||||
]);
|
||||
$pivotId = getWorkspaceUserId($workspace, $manager);
|
||||
|
||||
$response = $this->actingAs($owner)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Rôle mis à jour');
|
||||
|
||||
$updatedPivot = WorkspaceUser::find($pivotId);
|
||||
expect($updatedPivot->role->is(WorkspaceUserRole::Worker))->toBeTrue();
|
||||
});
|
||||
|
||||
// ── Role change resets permissions ──────────────────────────
|
||||
|
||||
test('role change resets permissions to defaults for new role', function () {
|
||||
[$owner, $workspace] = setupRoleTestUser(WorkspaceUserRole::Owner);
|
||||
$worker = createWorkspaceMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getWorkspaceUserId($workspace, $worker);
|
||||
|
||||
$this->actingAs($owner)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$updatedPivot = WorkspaceUser::find($pivotId);
|
||||
$managerDefaults = config('permissions.defaults.manager');
|
||||
expect($updatedPivot->permissions)->toBe($managerDefaults);
|
||||
|
||||
// Now change back to worker — permissions should be empty
|
||||
$this->actingAs($owner)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$updatedPivot->refresh();
|
||||
expect($updatedPivot->permissions)->toBe([]);
|
||||
});
|
||||
|
||||
// ── Owner cannot change own role ────────────────────────────
|
||||
|
||||
test('owner cannot change own role', function () {
|
||||
[$owner, $workspace] = setupRoleTestUser(WorkspaceUserRole::Owner);
|
||||
$pivotId = getWorkspaceUserId($workspace, $owner);
|
||||
|
||||
$response = $this->actingAs($owner)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Manager with can_manage_team can change worker role ─────
|
||||
|
||||
test('manager with can_manage_team can change worker role', function () {
|
||||
[$manager, $workspace] = setupRoleTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
$worker = createWorkspaceMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getWorkspaceUserId($workspace, $worker);
|
||||
|
||||
$response = $this->actingAs($manager)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Rôle mis à jour');
|
||||
});
|
||||
|
||||
// ── Manager with can_manage_team can change another manager role ─
|
||||
|
||||
test('manager with can_manage_team can change another manager role', function () {
|
||||
[$manager, $workspace] = setupRoleTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
$otherManager = createWorkspaceMember($workspace, WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
]);
|
||||
$pivotId = getWorkspaceUserId($workspace, $otherManager);
|
||||
|
||||
$response = $this->actingAs($manager)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$response->assertSessionHas('success', 'Rôle mis à jour');
|
||||
|
||||
$updatedPivot = WorkspaceUser::find($pivotId);
|
||||
expect($updatedPivot->role->is(WorkspaceUserRole::Worker))->toBeTrue();
|
||||
});
|
||||
|
||||
// ── Manager with can_manage_team cannot change owner role ───
|
||||
|
||||
test('manager with can_manage_team cannot change owner role', function () {
|
||||
[$manager, $workspace] = setupRoleTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => true,
|
||||
]);
|
||||
$owner = createWorkspaceMember($workspace, WorkspaceUserRole::Owner);
|
||||
$pivotId = getWorkspaceUserId($workspace, $owner);
|
||||
|
||||
$response = $this->actingAs($manager)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'worker',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Manager without can_manage_team gets 404 ───────────────
|
||||
|
||||
test('manager without can_manage_team gets 404 on role change', function () {
|
||||
[$manager, $workspace] = setupRoleTestUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
]);
|
||||
$worker = createWorkspaceMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getWorkspaceUserId($workspace, $worker);
|
||||
|
||||
$response = $this->actingAs($manager)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Worker gets 404 on role change attempt ──────────────────
|
||||
|
||||
test('worker gets 404 on role change attempt', function () {
|
||||
[$worker, $workspace] = setupRoleTestUser(WorkspaceUserRole::Worker);
|
||||
$otherWorker = createWorkspaceMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getWorkspaceUserId($workspace, $otherWorker);
|
||||
|
||||
$response = $this->actingAs($worker)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
// ── Activity log ────────────────────────────────────────────
|
||||
|
||||
test('role change is logged in activity log', function () {
|
||||
[$owner, $workspace] = setupRoleTestUser(WorkspaceUserRole::Owner);
|
||||
$worker = createWorkspaceMember($workspace, WorkspaceUserRole::Worker);
|
||||
$pivotId = getWorkspaceUserId($workspace, $worker);
|
||||
|
||||
$this->actingAs($owner)->patch(route('team.updateRole', $pivotId), [
|
||||
'role' => 'manager',
|
||||
]);
|
||||
|
||||
$log = Activity::latest('id')->first();
|
||||
expect($log->description)->toBe('role_changed')
|
||||
->and($log->properties['target_user'])->toBe($worker->name)
|
||||
->and($log->properties['old_role'])->toBe('worker')
|
||||
->and($log->properties['new_role'])->toBe('manager');
|
||||
});
|
||||
68
tests/Feature/Team/WorkspaceScopeTest.php
Normal file
68
tests/Feature/Team/WorkspaceScopeTest.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
use App\Concerns\HasWorkspaceScope;
|
||||
use App\Models\Client;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
function createScopeChecker(): object
|
||||
{
|
||||
return new class
|
||||
{
|
||||
use HasWorkspaceScope;
|
||||
|
||||
public function getWorkspace(): \App\Models\Workspace
|
||||
{
|
||||
return $this->currentWorkspace();
|
||||
}
|
||||
|
||||
public function checkAccess(\Illuminate\Database\Eloquent\Model $resource): void
|
||||
{
|
||||
$this->authorizeWorkspaceAccess($resource);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test('currentWorkspace resolves workspace from session', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user->id, ['role' => 'owner']);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createScopeChecker();
|
||||
$resolved = $checker->getWorkspace();
|
||||
|
||||
expect($resolved->id)->toBe($workspace->id);
|
||||
});
|
||||
|
||||
test('currentWorkspace fails when user not in workspace', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create();
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createScopeChecker();
|
||||
$checker->getWorkspace();
|
||||
})->throws(Illuminate\Database\Eloquent\ModelNotFoundException::class);
|
||||
|
||||
test('authorizeWorkspaceAccess passes for matching workspace', function () {
|
||||
$workspace = Workspace::factory()->create();
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace->id]);
|
||||
session(['current_workspace_id' => $workspace->id]);
|
||||
|
||||
$checker = createScopeChecker();
|
||||
$checker->checkAccess($client);
|
||||
|
||||
expect(true)->toBeTrue(); // No exception thrown
|
||||
});
|
||||
|
||||
test('authorizeWorkspaceAccess aborts 404 for mismatched workspace', function () {
|
||||
$workspace1 = Workspace::factory()->create();
|
||||
$workspace2 = Workspace::factory()->create();
|
||||
$client = Client::factory()->create(['workspace_id' => $workspace1->id]);
|
||||
session(['current_workspace_id' => $workspace2->id]);
|
||||
|
||||
$checker = createScopeChecker();
|
||||
$checker->checkAccess($client);
|
||||
})->throws(Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class);
|
||||
123
tests/Feature/Workspace/WorkspaceSwitchTest.php
Normal file
123
tests/Feature/Workspace/WorkspaceSwitchTest.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\Client;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
|
||||
function setupSwitchTestUser(string $role, int $workspaceCount = 2): array
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$workspaces = [];
|
||||
|
||||
for ($i = 0; $i < $workspaceCount; $i++) {
|
||||
$workspace = Workspace::factory()->create();
|
||||
$workspace->users()->attach($user->id, [
|
||||
'role' => $role,
|
||||
'permissions' => [],
|
||||
]);
|
||||
$workspaces[] = $workspace;
|
||||
}
|
||||
|
||||
return [$user, $workspaces];
|
||||
}
|
||||
|
||||
test('owner with multiple workspaces can switch workspace', function () {
|
||||
[$user, $workspaces] = setupSwitchTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withSession(['current_workspace_id' => $workspaces[0]->id])
|
||||
->post(route('workspace.switch'), [
|
||||
'workspace_id' => $workspaces[1]->id,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('dashboard'));
|
||||
|
||||
expect(session('current_workspace_id'))->toBe($workspaces[1]->id);
|
||||
});
|
||||
|
||||
test('workspace switching logs activity with previous and new workspace ids', function () {
|
||||
[$user, $workspaces] = setupSwitchTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['current_workspace_id' => $workspaces[0]->id])
|
||||
->post(route('workspace.switch'), [
|
||||
'workspace_id' => $workspaces[1]->id,
|
||||
]);
|
||||
|
||||
$log = Activity::latest('id')->first();
|
||||
|
||||
expect($log->description)->toBe('Switched workspace')
|
||||
->and($log->causer_id)->toBe($user->id)
|
||||
->and($log->properties['previous_workspace_id'])->toBe($workspaces[0]->id)
|
||||
->and($log->properties['new_workspace_id'])->toBe($workspaces[1]->id);
|
||||
});
|
||||
|
||||
test('user cannot switch to a workspace they do not belong to', function () {
|
||||
[$user, $workspaces] = setupSwitchTestUser(WorkspaceUserRole::Worker, 1);
|
||||
$otherWorkspace = Workspace::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withSession(['current_workspace_id' => $workspaces[0]->id])
|
||||
->post(route('workspace.switch'), [
|
||||
'workspace_id' => $otherWorkspace->id,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('dashboard'));
|
||||
|
||||
expect(session('current_workspace_id'))->toBe($workspaces[0]->id)
|
||||
->and(Activity::where('description', 'Switched workspace')->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('switching updates auth shared props on next page load', function () {
|
||||
[$user, $workspaces] = setupSwitchTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['current_workspace_id' => $workspaces[0]->id])
|
||||
->post(route('workspace.switch'), [
|
||||
'workspace_id' => $workspaces[1]->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('dashboard'));
|
||||
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->where('auth.currentWorkspace.id', $workspaces[1]->id)
|
||||
->where('auth.workspaceRole', 'owner')
|
||||
->has('auth.workspaceSwitchUrl')
|
||||
);
|
||||
});
|
||||
|
||||
test('user with single workspace can post switch with same workspace id without error', function () {
|
||||
[$user, $workspaces] = setupSwitchTestUser(WorkspaceUserRole::Worker, 1);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withSession(['current_workspace_id' => $workspaces[0]->id])
|
||||
->post(route('workspace.switch'), [
|
||||
'workspace_id' => $workspaces[0]->id,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('dashboard'));
|
||||
|
||||
expect(session('current_workspace_id'))->toBe($workspaces[0]->id);
|
||||
});
|
||||
|
||||
test('cross-workspace isolation: after switching, data queries return only new workspace data', function () {
|
||||
[$user, $workspaces] = setupSwitchTestUser(WorkspaceUserRole::Owner);
|
||||
|
||||
$clientA = Client::factory()->create(['workspace_id' => $workspaces[0]->id]);
|
||||
$clientB = Client::factory()->create(['workspace_id' => $workspaces[1]->id]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['current_workspace_id' => $workspaces[0]->id])
|
||||
->post(route('workspace.switch'), [
|
||||
'workspace_id' => $workspaces[1]->id,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('clients.index'));
|
||||
|
||||
$response->assertInertia(fn ($page) => $page
|
||||
->has('clients.data', 1)
|
||||
->where('clients.data.0.id', $clientB->id)
|
||||
);
|
||||
});
|
||||
92
tests/Unit/PermissionCheckTest.php
Normal file
92
tests/Unit/PermissionCheckTest.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use App\Concerns\AuthorizesPermissions;
|
||||
use App\Enums\Permission;
|
||||
use App\Enums\WorkspaceUserRole;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
||||
uses(Tests\TestCase::class, Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||||
|
||||
// Create a testable class that uses the trait
|
||||
function createPermissionChecker(): object
|
||||
{
|
||||
return new class
|
||||
{
|
||||
use AuthorizesPermissions;
|
||||
|
||||
public function check(string $permission): void
|
||||
{
|
||||
$this->authorizePermission($permission);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setupWorkspaceUser(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];
|
||||
}
|
||||
|
||||
test('owner always passes permission check', function () {
|
||||
[$user] = setupWorkspaceUser(WorkspaceUserRole::Owner);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createPermissionChecker();
|
||||
$checker->check(Permission::CanManageTeam);
|
||||
|
||||
expect(true)->toBeTrue(); // No exception thrown
|
||||
});
|
||||
|
||||
test('worker always fails permission check with 404', function () {
|
||||
[$user] = setupWorkspaceUser(WorkspaceUserRole::Worker);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createPermissionChecker();
|
||||
$checker->check(Permission::CanManageTeam);
|
||||
})->throws(Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class);
|
||||
|
||||
test('manager with granted permission passes', function () {
|
||||
[$user] = setupWorkspaceUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanViewActivityLogs => true,
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createPermissionChecker();
|
||||
$checker->check(Permission::CanViewActivityLogs);
|
||||
|
||||
expect(true)->toBeTrue(); // No exception thrown
|
||||
});
|
||||
|
||||
test('manager with denied permission fails with 404', function () {
|
||||
[$user] = setupWorkspaceUser(WorkspaceUserRole::Manager, [
|
||||
Permission::CanManageTeam => false,
|
||||
]);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createPermissionChecker();
|
||||
$checker->check(Permission::CanManageTeam);
|
||||
})->throws(Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class);
|
||||
|
||||
test('manager with unknown permission key defaults to false and fails with 404', function () {
|
||||
[$user] = setupWorkspaceUser(WorkspaceUserRole::Manager, []);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createPermissionChecker();
|
||||
$checker->check('some_unknown_permission');
|
||||
})->throws(Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class);
|
||||
|
||||
test('manager with empty permissions fails with 404', function () {
|
||||
[$user] = setupWorkspaceUser(WorkspaceUserRole::Manager, []);
|
||||
$this->actingAs($user);
|
||||
|
||||
$checker = createPermissionChecker();
|
||||
$checker->check(Permission::CanConfigurePortal);
|
||||
})->throws(Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class);
|
||||
Reference in New Issue
Block a user