projectContext(); $this->withApplicationKey($user) ->getJson(route('api.projects.settings.board', $project)) ->assertOk() ->assertJsonPath('data.backlogTicketStatuses.0.name', 'Backlog'); $columnId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.board.columns.store', $project), [ 'name' => 'In Review', ]) ->assertCreated() ->assertJsonPath('data.name', 'In Review') ->json('data.id'); $this->withApplicationKey($user) ->patchJson(route('api.projects.settings.board.columns.update', [$project, $columnId]), [ 'name' => 'Review', 'position' => 1, ]) ->assertOk() ->assertJsonPath('data.name', 'Review'); $statusId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.board.statuses.store', $project), [ 'board_column_id' => $columnId, 'name' => 'Ready', ]) ->assertCreated() ->assertJsonPath('data.name', 'Ready') ->json('data.id'); $replacementStatusId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.board.statuses.store', $project), [ 'board_column_id' => $columnId, 'name' => 'QA', ]) ->assertCreated() ->json('data.id'); $backlogStatus = $project->ticketStatuses()->whereNull('board_column_id')->firstOrFail(); $this->withApplicationKey($user) ->patchJson(route('api.projects.settings.board.statuses.transitions.update', [$project, $backlogStatus]), [ 'allowed_ticket_status_ids' => [$statusId], ]) ->assertOk() ->assertJsonPath('data.allowed_target_statuses.0.id', $statusId); $this->withApplicationKey($user) ->deleteJson(route('api.projects.settings.board.statuses.destroy', [$project, $statusId])) ->assertNoContent(); $this->assertDatabaseHas('ticket_statuses', [ 'id' => $replacementStatusId, ]); $this->withApplicationKey($user) ->deleteJson(route('api.projects.settings.board.columns.destroy', [$project, $columnId])) ->assertNoContent(); } public function test_metadata_settings_can_be_read_and_mutated(): void { [$user, $project] = $this->projectContext(); $this->withApplicationKey($user) ->getJson(route('api.projects.settings.metadata', $project)) ->assertOk() ->assertJsonPath('data.ticketTypes.0.name', 'Task'); $this->withApplicationKey($user) ->patchJson(route('api.projects.settings.metadata.update', $project), [ 'show_author' => false, 'show_story_points' => false, ]) ->assertOk() ->assertJsonPath('data.show_author', false); $typeId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.metadata.types.store', $project), [ 'is_parent' => true, 'name' => 'Epic', ]) ->assertCreated() ->assertJsonPath('data.name', 'Epic') ->json('data.id'); $this->withApplicationKey($user) ->patchJson(route('api.projects.settings.metadata.types.update', [$project, $typeId]), [ 'name' => 'Initiative', 'position' => 1, ]) ->assertOk() ->assertJsonPath('data.name', 'Initiative'); $priorityId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.metadata.priorities.store', $project), [ 'name' => 'Urgent', ]) ->assertCreated() ->json('data.id'); $fieldId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.metadata.fields.store', $project), [ 'field_type' => 'number', 'label' => 'Risk', ]) ->assertCreated() ->json('data.id'); $linkTypeId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.metadata.link-types.store', $project), [ 'name' => 'Relates to', ]) ->assertCreated() ->json('data.id'); $this->withApplicationKey($user) ->deleteJson(route('api.projects.settings.metadata.fields.destroy', [$project, $fieldId])) ->assertNoContent(); $this->withApplicationKey($user) ->deleteJson(route('api.projects.settings.metadata.link-types.destroy', [$project, $linkTypeId])) ->assertNoContent(); $this->withApplicationKey($user) ->deleteJson(route('api.projects.settings.metadata.priorities.destroy', [$project, $priorityId])) ->assertNoContent(); $this->withApplicationKey($user) ->deleteJson(route('api.projects.settings.metadata.types.destroy', [$project, $typeId])) ->assertNoContent(); } public function test_project_payloads_only_return_users_who_can_view_the_project(): void { [$user, $project] = $this->projectContext(); $viewer = User::factory()->create(['name' => 'Visible User']); User::factory()->create(['name' => 'Hidden User']); $teamViewer = User::factory()->create(['name' => 'Team Viewer']); $team = Team::factory()->create(); $this->syncProjectPermissions($project, $viewer, ['view']); $team->users()->attach($teamViewer); $this->syncProjectPermissions($project, $team, ['view']); $ticket = $project->tickets()->firstOrCreate([ 'sequence_number' => 1, ], [ 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $project->ticketPriorities()->firstOrFail()->id, 'ticket_status_id' => $project->ticketStatuses()->firstOrFail()->id, 'ticket_type_id' => $project->ticketTypes()->where('is_parent', false)->firstOrFail()->id, 'title' => 'Scoped users ticket', ]); $this->withApplicationKey($user) ->getJson(route('api.projects.backlog', $project)) ->assertOk() ->assertJsonPath('data.users.0.name', $user->name) ->assertJsonPath('data.users.1.name', 'Team Viewer') ->assertJsonPath('data.users.2.name', 'Visible User') ->assertJsonCount(3, 'data.users'); $this->withApplicationKey($user) ->getJson(route('api.projects.tickets.show', [$project, $ticket])) ->assertOk() ->assertJsonPath('data.users.0.name', $user->name) ->assertJsonPath('data.users.1.name', 'Team Viewer') ->assertJsonPath('data.users.2.name', 'Visible User') ->assertJsonCount(3, 'data.users'); } public function test_project_repositories_and_user_git_connections_are_available(): void { Http::preventStrayRequests(); Http::fake([ 'https://git.example.test/api/v1/user' => Http::response([ 'id' => 101, 'login' => 'api-user', 'full_name' => 'API User', ]), 'https://git.example.test/api/v1/user/repos*' => Http::response([ [ 'id' => 501, 'name' => 'kanbangit', 'full_name' => 'core-team/kanbangit', 'html_url' => 'https://git.example.test/core-team/kanbangit', 'owner' => [ 'login' => 'core-team', ], 'private' => true, ], ]), ]); [$user, $project] = $this->projectContext(); $gitConnectionId = $this->withApplicationKey($user) ->postJson(route('api.git-connections.store'), [ 'api_token' => 'secret-token', 'base_url' => 'https://git.example.test/api/v1', 'provider' => 'gitea', ]) ->assertCreated() ->assertJsonPath('data.account_login', 'api-user') ->json('data.id'); $this->withApplicationKey($user) ->getJson(route('api.git-connections.index')) ->assertOk() ->assertJsonPath('data.0.id', $gitConnectionId); $repositoryConnectionId = $this->withApplicationKey($user) ->postJson(route('api.projects.settings.repositories.store', $project), [ 'provider_repository_id' => '501', 'user_git_connection_id' => $gitConnectionId, ]) ->assertCreated() ->assertJsonPath('data.repository_full_name', 'core-team/kanbangit') ->json('data.id'); $this->withApplicationKey($user) ->getJson(route('api.projects.settings.repositories', $project)) ->assertOk() ->assertJsonPath('data.repositoryConnections.0.id', $repositoryConnectionId) ->assertJsonPath('data.availableRepositorySources.0.repositories.0.full_name', 'core-team/kanbangit'); $this->withApplicationKey($user) ->deleteJson(route('api.projects.settings.repositories.destroy', [$project, $repositoryConnectionId])) ->assertNoContent(); $this->withApplicationKey($user) ->deleteJson(route('api.git-connections.destroy', $gitConnectionId)) ->assertNoContent(); } /** * @return array{0: User, 1: Project} */ private function projectContext(): array { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $this->syncProjectPermissions($project, $user, ['edit', 'view']); return [$user, $project]; } }