get(route('projects.index')) ->assertRedirect(route('login')); $this->get(route('projects.create')) ->assertRedirect(route('login')); } /** * Ensure authenticated users can view the projects index. */ public function test_authenticated_users_can_view_the_projects_index(): void { $user = User::factory()->admin()->create(); $firstProject = Project::factory()->create([ 'name' => 'Core Platform', 'key' => 'CORE', ]); $secondProject = Project::factory()->create([ 'name' => 'Authentication', 'key' => 'AUTH', ]); $this->actingAs($user) ->get(route('projects.index')) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Index') ->has('projects', 2) ->where('projects.0.id', $secondProject->id) ->where('projects.1.id', $firstProject->id), ); } /** * Ensure non-admin users only see projects they can access directly or through teams. */ public function test_users_only_see_accessible_projects_on_the_index(): void { $user = User::factory()->create(); $team = Team::factory()->create([ 'name' => 'Platform Team', ]); $team->users()->attach($user); $directProject = Project::factory()->create([ 'name' => 'Direct Access', 'key' => 'DIR', ]); $teamProject = Project::factory()->create([ 'name' => 'Team Access', 'key' => 'TEAM', ]); Project::factory()->create([ 'name' => 'Hidden Project', 'key' => 'HIDE', ]); $projectPermissionService = app(ProjectPermissionService::class); $projectPermissionService->syncUserPermissions($directProject, $user, ['view']); $projectPermissionService->syncTeamPermissions($teamProject, $team, ['view']); $this->actingAs($user) ->get(route('projects.index')) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Index') ->has('projects', 2) ->where('projects.0.id', $teamProject->id) ->where('projects.1.id', $directProject->id), ); } /** * Ensure authenticated users can view the project creation screen. */ public function test_authenticated_users_can_view_the_project_creation_screen(): void { $user = User::factory()->create(); $this->actingAs($user) ->get(route('projects.create')) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Create'), ); } /** * Ensure project keys are normalized to uppercase on creation. */ public function test_authenticated_users_can_create_projects_with_uppercase_keys(): void { $user = User::factory()->create(); $response = $this->actingAs($user) ->post(route('projects.store'), [ 'name' => 'Kanban Foundation', 'key' => 'core', ]); $project = Project::query()->firstOrFail(); $response->assertRedirect(route('projects.show', $project)); $this->assertDatabaseHas('projects', [ 'name' => 'Kanban Foundation', 'key' => 'CORE', ]); foreach (ProjectPermissionService::permissions() as $permission) { $this->assertDatabaseHas('project_user_permissions', [ 'project_id' => $project->id, 'user_id' => $user->id, 'permission' => $permission, ]); } $this->assertDatabaseHas('ticket_types', [ 'project_id' => $project->id, 'name' => 'Task', 'system_key' => 'task', 'is_builtin' => true, ]); $this->assertDatabaseHas('ticket_types', [ 'project_id' => $project->id, 'name' => 'Parent', 'system_key' => 'parent', 'is_builtin' => true, ]); } /** * Ensure project keys remain unique after normalization. */ public function test_project_keys_must_be_unique_case_insensitively(): void { $user = User::factory()->create(); Project::factory()->create([ 'key' => 'AUTH', ]); $this->actingAs($user) ->from(route('projects.create')) ->post(route('projects.store'), [ 'name' => 'Another Auth Project', 'key' => 'auth', ]) ->assertSessionHasErrors('key') ->assertRedirect(route('projects.create')); } /** * Ensure project keys cannot exceed five characters. */ public function test_project_keys_may_not_exceed_five_characters(): void { $user = User::factory()->create(); $this->actingAs($user) ->from(route('projects.create')) ->post(route('projects.store'), [ 'name' => 'Too Long', 'key' => 'KANBAN', ]) ->assertSessionHasErrors('key') ->assertRedirect(route('projects.create')); } /** * Ensure authenticated users can view the project hub page. */ public function test_authenticated_users_can_view_a_project_hub_page(): void { $user = User::factory()->create(); $project = Project::factory()->create([ 'name' => 'Core Platform', 'key' => 'CORE', ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['view']); $this->actingAs($user) ->get(route('projects.show', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Show') ->where('canManageAccess', false) ->where('project.id', $project->id) ->where('project.key', 'CORE'), ); } /** * Ensure team-inherited permissions allow users to open a project. */ public function test_team_permissions_allow_users_to_view_a_project(): void { $user = User::factory()->create(); $team = Team::factory()->create(); $project = Project::factory()->create([ 'key' => 'TEAM', ]); $team->users()->attach($user); app(ProjectPermissionService::class)->syncTeamPermissions($project, $team, ['view']); $this->actingAs($user) ->get(route('projects.show', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Show') ->where('project.id', $project->id), ); } /** * Ensure users without direct or inherited access are forbidden. */ public function test_users_without_permission_cannot_view_a_project(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $this->actingAs($user) ->get(route('projects.show', $project)) ->assertForbidden(); } /** * Ensure edit access allows opening the project access management page. */ public function test_users_with_edit_permission_can_view_project_access_management(): void { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); $team = Team::factory()->create([ 'name' => 'Platform Team', ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit']); $this->actingAs($user) ->get(route('projects.access', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Access') ->where('project.id', $project->id) ->has('users', 1) ->has('teams', 1) ->where('permissionOptions.0.value', 'create'), ); } /** * Ensure admins can open the general project settings page. */ public function test_admins_can_view_general_project_settings(): void { $admin = User::factory()->admin()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); $this->actingAs($admin) ->get(route('projects.settings.general', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/settings/General') ->where('project.id', $project->id) ->where('project.key', 'CORE'), ); } /** * Ensure non-admin users cannot open general project settings even with project permissions. */ public function test_non_admin_users_cannot_view_general_project_settings(): void { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['view', 'edit', 'delete']); $this->actingAs($user) ->get(route('projects.settings.general', $project)) ->assertForbidden(); } /** * Ensure admins can delete projects and their dependent project-scoped data. */ public function test_admins_can_delete_projects_from_general_settings(): void { $admin = User::factory()->admin()->create(); $member = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', 'name' => 'Core Platform', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'Medium', 'position' => 1, ]); $boardColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'In Progress', 'position' => 1, ]); $ticket = Ticket::factory()->create([ 'board_column_id' => $boardColumn->id, 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $project->ticketTypes()->where('system_key', 'task')->firstOrFail()->id, 'title' => 'Delete me', ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $member, ['view']); $this->actingAs($admin) ->delete(route('projects.destroy', $project), [ 'confirmation_key' => 'CORE', ]) ->assertRedirect(route('projects.index')) ->assertSessionHas('status', 'Project Core Platform was deleted.'); $this->assertDatabaseMissing('projects', [ 'id' => $project->id, ]); $this->assertDatabaseMissing('board_columns', [ 'id' => $boardColumn->id, ]); $this->assertDatabaseMissing('tickets', [ 'id' => $ticket->id, ]); $this->assertDatabaseMissing('project_user_permissions', [ 'project_id' => $project->id, 'user_id' => $member->id, 'permission' => 'view', ]); } /** * Ensure project deletion requires the exact project key confirmation. */ public function test_admins_must_confirm_the_exact_project_key_to_delete_a_project(): void { $admin = User::factory()->admin()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); $this->actingAs($admin) ->from(route('projects.settings.general', $project)) ->delete(route('projects.destroy', $project), [ 'confirmation_key' => 'core', ]) ->assertRedirect(route('projects.settings.general', $project)) ->assertSessionHasErrors('confirmation_key'); $this->assertDatabaseHas('projects', [ 'id' => $project->id, ]); } /** * Ensure non-admin users cannot delete projects. */ public function test_non_admin_users_cannot_delete_projects(): void { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['view', 'edit', 'delete']); $this->actingAs($user) ->delete(route('projects.destroy', $project), [ 'confirmation_key' => 'CORE', ]) ->assertForbidden(); $this->assertDatabaseHas('projects', [ 'id' => $project->id, ]); } /** * Ensure admins can update direct project permissions for a user. */ public function test_admins_can_update_direct_project_permissions(): void { $admin = User::factory()->admin()->create(); $member = User::factory()->create([ 'name' => 'Regular User', ]); $project = Project::factory()->create([ 'key' => 'AUTH', ]); $this->actingAs($admin) ->patch(route('projects.permissions.users.update', [$project, $member]), [ 'permissions' => ['view', 'edit'], ]) ->assertRedirect(route('projects.access', $project)) ->assertSessionHas('status', 'Regular User permissions were updated.'); $this->assertDatabaseHas('project_user_permissions', [ 'project_id' => $project->id, 'user_id' => $member->id, 'permission' => 'view', ]); $this->assertDatabaseHas('project_user_permissions', [ 'project_id' => $project->id, 'user_id' => $member->id, 'permission' => 'edit', ]); } /** * Ensure users with project edit access can update team permissions. */ public function test_users_with_edit_permission_can_update_team_permissions(): void { $manager = User::factory()->create(); $team = Team::factory()->create([ 'name' => 'Delivery Team', ]); $project = Project::factory()->create([ 'key' => 'OPS', ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $manager, ['edit']); $this->actingAs($manager) ->patch(route('projects.permissions.teams.update', [$project, $team]), [ 'permissions' => ['view', 'create'], ]) ->assertRedirect(route('projects.access', $project)) ->assertSessionHas('status', 'Delivery Team permissions were updated.'); $this->assertDatabaseHas('project_team_permissions', [ 'project_id' => $project->id, 'team_id' => $team->id, 'permission' => 'view', ]); $this->assertDatabaseHas('project_team_permissions', [ 'project_id' => $project->id, 'team_id' => $team->id, 'permission' => 'create', ]); } }