create(); $project = Project::factory()->create(); $assignee = $user; if ($teamAccess) { $assignee = Team::factory()->create(); $assignee->users()->attach($user); } $this->syncProjectPermissions($project, $assignee, [$role]); $this->actingAs($user)->get(route('projects.board.index', $project)) ->assertOk()->assertInertia(fn (Assert $page) => $page ->where('navigation.project', [ 'id' => $project->id, 'name' => $project->name, 'key' => $project->key, 'canManage' => $canManage, 'canCreateTickets' => $canCreateTickets, ])); } /** @return array */ public static function projectRoles(): array { return [ 'reader' => ['read', false, false, false], 'writer' => ['write', false, false, true], 'project admin' => ['admin', false, true, true], 'team reader' => ['read', true, false, false], 'team writer' => ['write', true, false, true], 'team admin' => ['admin', true, true, true], ]; } public function test_global_admin_has_project_actions_without_direct_membership(): void { $user = User::factory()->admin()->create(); $project = Project::factory()->create(); $this->actingAs($user)->get(route('projects.show', $project)) ->assertOk()->assertInertia(fn (Assert $page) => $page ->where('navigation.project.canManage', true) ->where('navigation.project.canCreateTickets', true) ->where('auth.permissions.globalAdmin', true)); } public function test_every_project_admin_navigation_destination_is_authorized(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $this->syncProjectPermissions($project, $user, ['admin']); foreach (['projects.show', 'projects.board.index', 'projects.backlog.index', 'projects.settings.general', 'projects.settings.board', 'projects.settings.metadata', 'projects.settings.repositories', 'projects.settings.notifications', 'projects.access'] as $route) { $this->actingAs($user)->get(route($route, $project)) ->assertOk()->assertInertia(fn (Assert $page) => $page ->where('navigation.project.id', $project->id) ->where('navigation.project.canManage', true) ->where('auth.permissions.globalAdmin', false)); } } public function test_project_context_is_cleared_when_leaving_a_project(): void { $user = User::factory()->admin()->create(); $project = Project::factory()->create(); $this->actingAs($user)->get(route('projects.show', $project)) ->assertInertia(fn (Assert $page) => $page->where('navigation.project.id', $project->id)); $this->get(route('dashboard'))->assertOk() ->assertInertia(fn (Assert $page) => $page->where('navigation.project', null)); $this->get(route('projects.create'))->assertOk() ->assertInertia(fn (Assert $page) => $page->where('navigation.project', null)); } public function test_unrelated_projects_remain_inaccessible(): void { $user = User::factory()->create(); $project = Project::factory()->create(['name' => 'Private project']); $this->actingAs($user)->get(route('projects.show', $project)) ->assertForbidden()->assertDontSee('Private project'); } public function test_navigation_rendering_and_url_matching(): void { $process = new Process(['node', 'tests/Fixtures/navigation.mjs'], base_path()); $process->setTimeout(60); $process->run(); $this->assertTrue($process->isSuccessful(), $process->getOutput().$process->getErrorOutput()); $this->assertStringContainsString('Navigation behavior checks passed.', $process->getOutput()); } }