Http::response([ ['name' => 'main'], ['name' => 'release/1.0'], ]), ]); [$user, $project, $ticket] = $this->ticketContext(); RepositoryConnection::factory()->for($project)->create([ 'base_url' => 'https://git.example.test', 'owner_name' => 'core-team', 'provider' => GitProviderKey::Gitea, 'provider_repository_id' => '501', 'repository_full_name' => 'core-team/kanbangit', 'repository_name' => 'kanbangit', 'repository_url' => 'https://git.example.test/core-team/kanbangit', ]); UserGitConnection::factory()->for($user)->create([ 'base_url' => 'https://git.example.test', 'provider' => GitProviderKey::Gitea, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->getJson(route('projects.tickets.branches.validate', [ 'project' => $project, 'ticket' => $ticket, 'branch_name' => 'core-1-login-form', 'repository_connection_id' => $project->repositoryConnections()->firstOrFail()->id, ])) ->assertOk() ->assertExactJson([ 'is_valid' => true, 'message' => 'Branch name is available for CORE-1.', ]); } /** * Ensure branch validation reports existing branch names. */ public function test_branch_validation_rejects_existing_branch_names(): void { Http::preventStrayRequests(); Http::fake([ 'https://git.example.test/api/v1/repos/core-team/kanbangit/branches*' => Http::response([ ['name' => 'core-1-login-form'], ]), ]); [$user, $project, $ticket] = $this->ticketContext(); RepositoryConnection::factory()->for($project)->create([ 'base_url' => 'https://git.example.test', 'owner_name' => 'core-team', 'provider' => GitProviderKey::Gitea, 'provider_repository_id' => '501', 'repository_full_name' => 'core-team/kanbangit', 'repository_name' => 'kanbangit', 'repository_url' => 'https://git.example.test/core-team/kanbangit', ]); UserGitConnection::factory()->for($user)->create([ 'base_url' => 'https://git.example.test', 'provider' => GitProviderKey::Gitea, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->getJson(route('projects.tickets.branches.validate', [ 'project' => $project, 'ticket' => $ticket, 'branch_name' => 'core-1-login-form', 'repository_connection_id' => $project->repositoryConnections()->firstOrFail()->id, ])) ->assertOk() ->assertExactJson([ 'is_valid' => false, 'message' => 'This branch name already exists in the selected repository.', ]); } /** * Ensure inaccessible repositories remain visible but cannot be used for branch creation. */ public function test_branch_validation_requires_a_matching_personal_git_connection(): void { Http::preventStrayRequests(); Http::fake(); [$user, $project, $ticket] = $this->ticketContext(); RepositoryConnection::factory()->for($project)->create([ 'base_url' => 'https://git.example.test', 'owner_name' => 'core-team', 'provider' => GitProviderKey::Gitea, 'provider_repository_id' => '501', 'repository_full_name' => 'core-team/kanbangit', 'repository_name' => 'kanbangit', 'repository_url' => 'https://git.example.test/core-team/kanbangit', ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->getJson(route('projects.tickets.branches.validate', [ 'project' => $project, 'ticket' => $ticket, 'branch_name' => 'core-1-login-form', 'repository_connection_id' => $project->repositoryConnections()->firstOrFail()->id, ])) ->assertOk() ->assertExactJson([ 'is_valid' => false, 'message' => 'Connect a matching Git connection in your user settings first.', ]); } /** * Ensure users with edit permission can create branches from the ticket detail flow. */ public function test_users_with_edit_permission_can_create_ticket_branches(): void { Http::preventStrayRequests(); Http::fake([ 'https://git.example.test/api/v1/repos/core-team/kanbangit/branches*' => Http::sequence() ->push([ ['name' => 'main'], ], 200) ->push([ 'name' => 'core-1-login-form', ], 201), 'https://git.example.test/api/v1/repos/core-team/kanbangit' => Http::response([ 'id' => 501, 'name' => 'kanbangit', 'full_name' => 'core-team/kanbangit', 'html_url' => 'https://git.example.test/core-team/kanbangit', 'default_branch' => 'main', 'owner' => [ 'login' => 'core-team', ], 'private' => false, ]), ]); [$user, $project, $ticket] = $this->ticketContext(); $repositoryConnection = RepositoryConnection::factory()->for($project)->create([ 'base_url' => 'https://git.example.test', 'owner_name' => 'core-team', 'provider' => GitProviderKey::Gitea, 'provider_repository_id' => '501', 'repository_full_name' => 'core-team/kanbangit', 'repository_name' => 'kanbangit', 'repository_url' => 'https://git.example.test/core-team/kanbangit', ]); UserGitConnection::factory()->for($user)->create([ 'api_token' => 'branch-token', 'base_url' => 'https://git.example.test', 'provider' => GitProviderKey::Gitea, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->post(route('projects.tickets.branches.store', [$project, $ticket]), [ 'branch_name' => ' core-1-login-form ', 'repository_connection_id' => $repositoryConnection->id, ]) ->assertRedirect(route('projects.tickets.show', [$project, $ticket])) ->assertSessionHas('status', 'core-1-login-form was created in core-team/kanbangit.'); Http::assertSent(function ($request): bool { return $request->url() === 'https://git.example.test/api/v1/repos/core-team/kanbangit/branches' && $request['new_branch_name'] === 'core-1-login-form' && $request['old_ref_name'] === 'main' && $request->hasHeader('Authorization', 'token branch-token'); }); } /** * Ensure project edit permission is required for branch creation. */ public function test_branch_creation_requires_the_project_edit_permission(): void { Http::preventStrayRequests(); Http::fake(); [$user, $project, $ticket] = $this->ticketContext(); $repositoryConnection = RepositoryConnection::factory()->for($project)->create(); $this->actingAs($user) ->post(route('projects.tickets.branches.store', [$project, $ticket]), [ 'branch_name' => 'core-1-login-form', 'repository_connection_id' => $repositoryConnection->id, ]) ->assertForbidden(); } /** * @return array{0: User, 1: Project, 2: Ticket} */ private function ticketContext(): array { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'Medium', 'position' => 1, ]); $taskType = $project->ticketTypes()->where('system_key', 'task')->firstOrFail(); $ticket = Ticket::factory()->create([ 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $taskType->id, 'title' => 'Login form', ]); return [$user, $project, $ticket]; } }