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(); $ticketLinkType = TicketLinkType::factory()->for($project)->create([ 'label' => 'Relates to', 'position' => 1, ]); $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, ]); $linkedTicket = Ticket::factory()->create([ 'project_id' => $project->id, 'sequence_number' => 2, 'ticket_key' => 'CORE-2', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $taskType->id, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->post(route('projects.tickets.links.store', [$project, $ticket]), [ 'linked_ticket_id' => $linkedTicket->id, 'ticket_link_type_id' => $ticketLinkType->id, ]) ->assertRedirect(route('projects.tickets.show', [$project, $ticket])) ->assertSessionHas('status', 'Relates to link was saved.'); $this->assertDatabaseHas('ticket_links', [ 'source_ticket_id' => min($ticket->id, $linkedTicket->id), 'target_ticket_id' => max($ticket->id, $linkedTicket->id), 'ticket_link_type_id' => $ticketLinkType->id, ]); $this->assertDatabaseHas('ticket_worklog_entries', [ 'ticket_id' => $ticket->id, 'actor_id' => $user->id, 'entry_type' => 'link_updated', 'summary' => 'Linked CORE-2 as Relates to.', ]); } /** * Ensure ticket links cannot target the current ticket or a ticket from another project. */ public function test_ticket_links_must_target_other_tickets_within_the_same_project(): void { $user = User::factory()->create(); $project = Project::factory()->create(); $otherProject = Project::factory()->create(); app(InitializeProjectTicketMetadata::class)->handle($project); app(InitializeProjectTicketMetadata::class)->handle($otherProject); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'Medium', 'position' => 1, ]); $otherPriority = TicketPriority::factory()->for($otherProject)->create([ 'name' => 'Medium', 'position' => 1, ]); $taskType = $project->ticketTypes()->where('system_key', 'task')->firstOrFail(); $otherTaskType = $otherProject->ticketTypes()->where('system_key', 'task')->firstOrFail(); $ticketLinkType = TicketLinkType::factory()->for($project)->create([ 'label' => 'Relates to', 'position' => 1, ]); $ticket = Ticket::factory()->create([ 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'APP-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $taskType->id, ]); $foreignTicket = Ticket::factory()->create([ 'project_id' => $otherProject->id, 'sequence_number' => 1, 'ticket_key' => 'OPS-1', 'ticket_priority_id' => $otherPriority->id, 'ticket_type_id' => $otherTaskType->id, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->from(route('projects.tickets.show', [$project, $ticket])) ->post(route('projects.tickets.links.store', [$project, $ticket]), [ 'linked_ticket_id' => $ticket->id, 'ticket_link_type_id' => $ticketLinkType->id, ]) ->assertRedirect(route('projects.tickets.show', [$project, $ticket])) ->assertSessionHasErrors('linked_ticket_id'); $this->actingAs($user) ->from(route('projects.tickets.show', [$project, $ticket])) ->post(route('projects.tickets.links.store', [$project, $ticket]), [ 'linked_ticket_id' => $foreignTicket->id, 'ticket_link_type_id' => $ticketLinkType->id, ]) ->assertRedirect(route('projects.tickets.show', [$project, $ticket])) ->assertSessionHasErrors('linked_ticket_id'); } /** * Ensure users with edit permission can remove ticket links. */ public function test_users_with_edit_permission_can_remove_ticket_links(): void { $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(); $ticketLinkType = TicketLinkType::factory()->for($project)->create([ 'label' => 'Relates to', 'position' => 1, ]); $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, ]); $linkedTicket = Ticket::factory()->create([ 'project_id' => $project->id, 'sequence_number' => 2, 'ticket_key' => 'CORE-2', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $taskType->id, ]); $ticketLink = TicketLink::factory()->create([ 'source_ticket_id' => min($ticket->id, $linkedTicket->id), 'target_ticket_id' => max($ticket->id, $linkedTicket->id), 'ticket_link_type_id' => $ticketLinkType->id, ]); app(ProjectPermissionService::class)->syncUserPermissions($project, $user, ['edit', 'view']); $this->actingAs($user) ->delete(route('projects.tickets.links.destroy', [$project, $ticket, $ticketLink])) ->assertRedirect(route('projects.tickets.show', [$project, $ticket])) ->assertSessionHas('status', 'Relates to link was removed.'); $this->assertDatabaseMissing('ticket_links', [ 'id' => $ticketLink->id, ]); $this->assertDatabaseHas('ticket_worklog_entries', [ 'ticket_id' => $ticket->id, 'actor_id' => $user->id, 'entry_type' => 'link_removed', 'summary' => 'Removed Relates to link to CORE-2.', ]); } }