admin()->create(); $writer = User::factory()->create(); $member = User::factory()->create(); $outsider = User::factory()->create(); $project = Project::factory()->create(); $otherProject = Project::factory()->create(); $team = Team::factory()->create(); $otherTeam = Team::factory()->create(); $team->users()->attach([$writer->id, $member->id]); $otherTeam->users()->attach($writer); $service = app(PermissionService::class); $service->sync($writer, $project, [Permission::ProjectRead]); $service->sync($team, $project, [Permission::ProjectWrite]); $service->sync($otherTeam, $project, [Permission::ProjectRead]); $service->sync($outsider, $otherProject, [Permission::ProjectAdmin]); $service->sync($outsider, null, [Permission::ProjectCreate]); $service->sync($outsider, $team, [Permission::TeamAdmin]); $map = $service->projectAccessMap(User::all(), $project); foreach (User::all() as $user) { $expected = array_values(array_filter(Permission::projectAssignable(), fn (Permission $permission): bool => $service->allows($user, $permission, $project))); $this->assertSame(Permission::values($expected), $map['effectivePermissions'][$user->id]); } $this->assertSame([], $map['effectivePermissions'][$outsider->id]); $this->assertSame(['project:read', 'project:write'], $map['effectivePermissions'][$writer->id]); $this->assertSame([$admin->id], $map['globalAdminUserIds']); $this->actingAs($admin)->get(route('projects.access', $project))->assertOk() ->assertInertia(fn (Assert $page) => $page->component('projects/Access') ->where("effectivePermissions.{$writer->id}", ['project:read', 'project:write']) ->where("directPermissions.{$writer->id}", ['project:read']) ->where("teamPermissions.{$team->id}", ['project:write']) ->where('globalAdminUserIds', [$admin->id])); } public function test_bulk_access_summary_uses_a_bounded_number_of_queries(): void { $project = Project::factory()->create(); User::factory()->create(); $service = app(PermissionService::class); $countQueries = function () use ($service, $project): int { $users = User::all(); DB::enableQueryLog(); DB::flushQueryLog(); $service->projectAccessMap($users, $project); $count = count(DB::getQueryLog()); DB::disableQueryLog(); return $count; }; $initialCount = $countQueries(); $users = User::factory()->count(10)->create(); $team = Team::factory()->create(); $team->users()->attach($users); $service->sync($team, $project, [Permission::ProjectWrite]); $this->assertSame($initialCount, $countQueries()); $this->assertLessThanOrEqual(4, $initialCount); } public function test_readers_writers_and_unrelated_admins_cannot_manage_project_access(): void { $project = Project::factory()->create(); $other = Project::factory()->create(); $member = User::factory()->create(); $team = Team::factory()->create(); $service = app(PermissionService::class); $service->sync($member, $project, [Permission::ProjectRead]); $service->sync($team, $project, [Permission::ProjectWrite]); foreach ([[], [Permission::ProjectRead], [Permission::ProjectWrite]] as $permissions) { $actor = User::factory()->create(); $service->sync($actor, $project, $permissions); $service->sync($actor, $other, [Permission::ProjectAdmin]); $this->actingAs($actor)->get(route('projects.access', $project))->assertForbidden(); foreach ([['users', $member], ['teams', $team]] as [$kind, $assignee]) { $this->patch(route("projects.permissions.{$kind}.update", [$project, $assignee]), ['permissions' => ['project:admin']])->assertForbidden(); } } $this->assertSame(['project:read'], $service->assignmentMap(User::class, $project)[$member->id]); $this->assertSame(['project:write'], $service->assignmentMap(Team::class, $project)[$team->id]); } public function test_invalid_assignments_preserve_both_user_and_team_permissions(): void { $admin = User::factory()->admin()->create(); $project = Project::factory()->create(); $service = app(PermissionService::class); foreach ([['users', User::factory()->create()], ['teams', Team::factory()->create()]] as [$kind, $assignee]) { $service->sync($assignee, $project, [Permission::ProjectRead]); foreach ([['global:admin'], ['team:admin'], ['unknown'], ['project:read', 'project:read']] as $permissions) { $this->actingAs($admin)->patch(route("projects.permissions.{$kind}.update", [$project, $assignee]), ['permissions' => $permissions]) ->assertSessionHasErrors('permissions.0'); $this->assertSame(['project:read'], $service->assignmentMap($assignee::class, $project)[$assignee->id]); } } } public function test_clearing_assignments_preserves_access_from_other_sources(): void { $admin = User::factory()->admin()->create(); $member = User::factory()->create(); $project = Project::factory()->create(); $team = Team::factory()->create(); $team->users()->attach($member); $service = app(PermissionService::class); $service->sync($member, $project, [Permission::ProjectRead]); $service->sync($team, $project, [Permission::ProjectWrite]); $this->actingAs($admin)->patch(route('projects.permissions.users.update', [$project, $member]), ['permissions' => []]) ->assertRedirect(route('projects.access', $project))->assertSessionHasNoErrors(); $this->assertTrue($service->allows($member, Permission::ProjectWrite, $project)); $service->sync($member, $project, [Permission::ProjectRead]); $this->patch(route('projects.permissions.teams.update', [$project, $team]), ['permissions' => []]) ->assertRedirect(route('projects.access', $project))->assertSessionHasNoErrors(); $this->assertTrue($service->allows($member, Permission::ProjectRead, $project)); $this->assertFalse($service->allows($member, Permission::ProjectWrite, $project)); } public function test_removing_own_admin_access_redirects_to_projects_for_direct_and_team_grants(): void { $service = app(PermissionService::class); foreach (['users', 'teams'] as $kind) { $actor = User::factory()->create(); $project = Project::factory()->create(); $assignee = $actor; if ($kind === 'teams') { $assignee = Team::factory()->create(); $assignee->users()->attach($actor); } $service->sync($assignee, $project, [Permission::ProjectAdmin]); $this->actingAs($actor)->patch(route("projects.permissions.{$kind}.update", [$project, $assignee]), ['permissions' => []]) ->assertRedirect(route('projects.index'))->assertSessionHasNoErrors(); $this->assertFalse($service->allows($actor, Permission::ProjectAdmin, $project)); } } }