create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $todoColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'Todo', 'position' => 1, ]); $doingColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'Doing', 'position' => 2, ]); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'High', 'position' => 1, ]); Ticket::factory()->create([ 'board_column_id' => null, 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'title' => 'Backlog ticket', ]); $boardTicket = Ticket::factory()->create([ 'board_column_id' => $doingColumn->id, 'project_id' => $project->id, 'sequence_number' => 2, 'ticket_key' => 'CORE-2', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'title' => 'Board ticket', ]); $this->syncProjectPermissions($project, $user, ['view']); $this->actingAs($user) ->get(route('projects.board.index', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Board') ->where('project.id', $project->id) ->where('boardColumns.0.name', $todoColumn->name) ->has('boardColumns.0.tickets', 0) ->where('boardColumns.1.name', $doingColumn->name) ->has('boardColumns.1.tickets', 1) ->where('boardColumns.1.tickets.0.ticket_key', $boardTicket->ticket_key) ->where('boardColumns.1.tickets.0.title', $boardTicket->title) ->where('canEditTickets', false), ); } /** * Ensure moving backlog tickets into a board column requires edit permission and logs the change. */ public function test_users_with_edit_permission_can_move_backlog_tickets_to_a_board_column(): void { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $todoColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'Todo', 'position' => 1, ]); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'High', 'position' => 1, ]); $ticket = Ticket::factory()->create([ 'board_column_id' => null, 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'title' => 'Backlog ticket', ]); $this->syncProjectPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->patch(route('projects.tickets.status.update', [$project, $ticket]), [ 'board_column_id' => $todoColumn->id, 'redirect_to' => 'backlog', ]) ->assertRedirect(route('projects.backlog.index', $project)) ->assertSessionHas('status', 'CORE-1 moved to Todo.'); $this->assertDatabaseHas('tickets', [ 'id' => $ticket->id, 'board_column_id' => $todoColumn->id, ]); $this->assertDatabaseHas('ticket_worklog_entries', [ 'ticket_id' => $ticket->id, 'actor_id' => $user->id, 'entry_type' => 'status_changed', 'summary' => 'Moved from Backlog to Todo.', ]); } /** * Ensure board tickets can move between columns. */ public function test_users_with_edit_permission_can_move_board_tickets_between_columns(): void { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $todoColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'Todo', 'position' => 1, ]); $doneColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'Done', 'position' => 2, ]); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'High', 'position' => 1, ]); $ticket = Ticket::factory()->create([ 'board_column_id' => $todoColumn->id, 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'title' => 'Board ticket', ]); $this->syncProjectPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->patch(route('projects.tickets.status.update', [$project, $ticket]), [ 'board_column_id' => $doneColumn->id, 'redirect_to' => 'board', ]) ->assertRedirect(route('projects.board.index', $project)) ->assertSessionHas('status', 'CORE-1 moved to Done.'); $this->assertDatabaseHas('tickets', [ 'id' => $ticket->id, 'board_column_id' => $doneColumn->id, ]); $this->assertDatabaseHas('ticket_worklog_entries', [ 'ticket_id' => $ticket->id, 'actor_id' => $user->id, 'entry_type' => 'status_changed', 'summary' => 'Moved from Todo to Done.', ]); } /** * Ensure moving a board ticket back to backlog removes it from the board columns. */ public function test_users_with_edit_permission_can_move_board_tickets_back_to_backlog(): void { $user = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $doingColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'Doing', 'position' => 1, ]); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'High', 'position' => 1, ]); $ticket = Ticket::factory()->create([ 'board_column_id' => $doingColumn->id, 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'title' => 'Board ticket', ]); $this->syncProjectPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->patch(route('projects.tickets.status.update', [$project, $ticket]), [ 'board_column_id' => null, 'redirect_to' => 'board', ]) ->assertRedirect(route('projects.board.index', $project)) ->assertSessionHas('status', 'CORE-1 moved to Backlog.'); $this->assertDatabaseHas('tickets', [ 'id' => $ticket->id, 'board_column_id' => null, ]); $this->actingAs($user) ->get(route('projects.board.index', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->where('boardColumns.0.name', $doingColumn->name) ->has('boardColumns.0.tickets', 0), ); } /** * Ensure cross-project board column moves are rejected. */ public function test_status_moves_to_columns_from_other_projects_are_rejected(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $otherProject = Project::factory()->create(); app(InitializeProjectTicketMetadata::class)->handle($project); app(InitializeProjectTicketMetadata::class)->handle($otherProject); $otherProjectColumn = BoardColumn::factory()->for($otherProject)->create([ 'name' => 'Foreign', 'position' => 1, ]); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'High', 'position' => 1, ]); $ticket = Ticket::factory()->create([ 'board_column_id' => null, 'project_id' => $project->id, 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, ]); $this->syncProjectPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->from(route('projects.backlog.index', $project)) ->patch(route('projects.tickets.status.update', [$project, $ticket]), [ 'board_column_id' => $otherProjectColumn->id, 'redirect_to' => 'backlog', ]) ->assertRedirect(route('projects.backlog.index', $project)) ->assertSessionHasErrors('board_column_id'); $this->assertDatabaseHas('tickets', [ 'id' => $ticket->id, 'board_column_id' => null, ]); $this->assertDatabaseCount('ticket_worklog_entries', 0); } }