Guard directional ticket link writes #82

Merged
Twyco merged 1 commits from koda/ticket-link-counterparts-v2-review-fixes into koda/ticket-link-counterparts-v2 2026-05-24 10:01:19 +00:00
2 changed files with 66 additions and 0 deletions
Showing only changes of commit da0054a269 - Show all commits
@@ -29,6 +29,12 @@ class StoreProjectTicketLink
->findOrFail($linkedTicketId);
return DB::transaction(function () use ($linkedTicket, $user, $ticketLinkTypeId, $ticket): TicketLink {
Ticket::query()
->whereKey([$ticket->id, $linkedTicket->id])
->orderBy('id')
->lockForUpdate()
->get(['id']);
$ticketLink = TicketLink::query()
->where(function ($query) use ($ticket, $linkedTicket): void {
$query
@@ -146,6 +146,66 @@ class ProjectTicketLinkControllerTest extends TestCase
->assertSessionHasErrors('linked_ticket_id');
}
/**
* Ensure relinking an existing reverse-direction pair updates the existing row instead of creating a duplicate.
*/
public function test_linking_an_existing_reverse_direction_pair_reuses_the_same_row(): void
{
$user = User::factory()->create();
$project = Project::factory()->create([
'key' => 'CORE',
]);
app(InitializeProjectTicketMetadata::class)->handle($project);
$priority = $project->ticketPriorities()->firstOrFail();
$taskType = $project->ticketTypes()
->where('is_parent', false)
->orderBy('position')
->firstOrFail();
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
'label' => 'Blocks',
'counterpart_label' => 'Blocked by',
'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,
]);
$existingLink = TicketLink::factory()->create([
'source_ticket_id' => $linkedTicket->id,
'target_ticket_id' => $ticket->id,
'ticket_link_type_id' => $ticketLinkType->id,
]);
$this->syncProjectPermissions($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', 'Blocks link was saved.');
$this->assertSame(1, TicketLink::query()->count());
$existingLink->refresh();
$this->assertSame($ticket->id, $existingLink->source_ticket_id);
$this->assertSame($linkedTicket->id, $existingLink->target_ticket_id);
}
/**
* Ensure users with edit permission can remove ticket links.
*/