create(); $visibleProject = Project::factory()->create([ 'name' => 'Visible project', ]); Project::factory()->create([ 'name' => 'Hidden project', ]); $this->syncProjectPermissions($visibleProject, $user, ['view']); $this->withApplicationKey($user) ->getJson(route('api.projects.index')) ->assertOk() ->assertJsonPath('data.0.name', 'Visible project') ->assertJsonCount(1, 'data'); } public function test_users_with_project_create_permission_can_create_projects(): void { $user = User::factory()->create(); $this->syncGlobalPermissions($user, ['create']); $this->withApplicationKey($user) ->postJson(route('api.projects.store'), [ 'creation_mode' => 'scratch', 'key' => 'api', 'name' => 'API Project', ]) ->assertCreated() ->assertJsonPath('data.key', 'API') ->assertJsonPath('data.name', 'API Project') ->assertJsonPath('meta.message', 'API Project was created.'); $project = Project::query()->where('key', 'API')->firstOrFail(); $project->refresh(); $this->assertSame(2, $project->ticketTypes()->count()); $this->assertSame(1, $project->ticketStatuses()->count()); $this->assertNotNull($project->project_template_id); } public function test_users_can_create_projects_from_templates_via_api(): void { $user = User::factory()->create(); $this->syncGlobalPermissions($user, ['create']); $template = ProjectTemplate::factory()->create([ 'name' => 'API Template', 'slug' => 'api-template', 'copy_areas' => [ DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS, DuplicateProjectConfiguration::COPY_TICKET_TYPES, ], 'configuration_snapshot' => [ DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS => [ 'show_author' => false, 'show_story_points' => false, ], DuplicateProjectConfiguration::COPY_TICKET_TYPES => [ [ 'name' => 'Spike', 'position' => 1, 'is_parent' => false, ], ], ], ]); $this->withApplicationKey($user) ->postJson(route('api.projects.store'), [ 'key' => 'tpl1', 'name' => 'Template API Project', 'creation_mode' => 'template', 'source_template_id' => $template->id, ]) ->assertCreated() ->assertJsonPath('data.key', 'TPL1') ->assertJsonPath('data.name', 'Template API Project'); $project = Project::query()->where('key', 'TPL1')->firstOrFail(); $project->refresh(); $this->assertSame($template->id, $project->project_template_id); $this->assertFalse($project->show_author); $this->assertFalse($project->show_story_points); $this->assertSame(['Spike'], $project->ticketTypes()->pluck('name')->all()); } public function test_users_can_duplicate_projects_via_api(): void { $user = User::factory()->create(); $this->syncGlobalPermissions($user, ['create']); $sourceProject = Project::factory()->create([ 'key' => 'SRC1', 'show_author' => false, ]); $this->syncProjectPermissions($sourceProject, $user, ['view']); $sourceProject->ticketTypes()->delete(); $sourceProject->ticketStatuses()->delete(); $sourceProject->boardColumns()->delete(); $column = BoardColumn::factory()->create([ 'project_id' => $sourceProject->id, 'name' => 'In Progress', 'position' => 1, ]); $backlogStatus = TicketStatus::factory()->create([ 'project_id' => $sourceProject->id, 'name' => 'Queued', 'position' => 1, ]); $activeStatus = TicketStatus::factory()->create([ 'project_id' => $sourceProject->id, 'board_column_id' => $column->id, 'name' => 'Active', 'position' => 1, ]); $backlogStatus->allowedTargetStatuses()->sync([$activeStatus->id]); TicketType::factory()->create([ 'project_id' => $sourceProject->id, 'name' => 'Spike', 'position' => 1, ]); $this->withApplicationKey($user) ->postJson(route('api.projects.store'), [ 'key' => 'dup2', 'name' => 'Duplicated API Project', 'creation_mode' => 'duplicate', 'source_project_id' => $sourceProject->id, 'copy_areas' => [ DuplicateProjectConfiguration::COPY_PROJECT_SETTINGS, DuplicateProjectConfiguration::COPY_BOARD_WORKFLOW, DuplicateProjectConfiguration::COPY_TICKET_TYPES, ], ]) ->assertCreated() ->assertJsonPath('data.key', 'DUP2') ->assertJsonPath('data.name', 'Duplicated API Project'); $project = Project::query()->where('key', 'DUP2')->firstOrFail(); $this->assertFalse($project->show_author); $this->assertSame(['In Progress'], $project->boardColumns()->pluck('name')->all()); $this->assertSame(['Queued', 'Active'], $project->ticketStatuses()->pluck('name')->all()); $this->assertSame(['Spike'], $project->ticketTypes()->pluck('name')->all()); $this->assertSame(['Active'], $project->ticketStatuses()->where('name', 'Queued')->firstOrFail()->allowedTargetStatuses()->pluck('name')->all()); } public function test_project_duplication_via_api_requires_visible_source_project(): void { $user = User::factory()->create(); $this->syncGlobalPermissions($user, ['create']); $sourceProject = Project::factory()->create(); $this->withApplicationKey($user) ->postJson(route('api.projects.store'), [ 'key' => 'nope', 'name' => 'No Access', 'creation_mode' => 'duplicate', 'source_project_id' => $sourceProject->id, ]) ->assertStatus(422) ->assertJsonValidationErrors(['source_project_id']); } public function test_project_show_and_delete_enforce_permissions(): void { $user = User::factory()->create(); $admin = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $this->withApplicationKey($user) ->getJson(route('api.projects.show', $project)) ->assertForbidden(); $this->syncProjectPermissions($project, $user, ['view']); $this->withApplicationKey($user) ->getJson(route('api.projects.show', $project)) ->assertOk() ->assertJsonPath('data.key', 'CORE'); $this->syncGlobalPermissions($admin, ['admin']); $this->withApplicationKey($admin) ->deleteJson(route('api.projects.destroy', $project), [ 'confirmation_key' => 'CORE', ]) ->assertNoContent(); $this->assertDatabaseMissing('projects', [ 'id' => $project->id, ]); } }