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:
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)
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user