Files
L-Ami-Fiduciaire/tests/Feature/Declaration/RoleBasedAccessTest.php
Saad Ibn-Ezzoubayr c89d1879bf 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>
2026-03-18 00:12:50 +00:00

378 lines
13 KiB
PHP

<?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)
);
});