Merge pull request 'Guard directional ticket link writes' (#82) from koda/ticket-link-counterparts-v2-review-fixes into koda/ticket-link-counterparts-v2
Reviewed-on: OpenSource/KanbanGit#82
This commit was merged in pull request #82.
This commit is contained in:
@@ -29,6 +29,12 @@ class StoreProjectTicketLink
|
|||||||
->findOrFail($linkedTicketId);
|
->findOrFail($linkedTicketId);
|
||||||
|
|
||||||
return DB::transaction(function () use ($linkedTicket, $user, $ticketLinkTypeId, $ticket): TicketLink {
|
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()
|
$ticketLink = TicketLink::query()
|
||||||
->where(function ($query) use ($ticket, $linkedTicket): void {
|
->where(function ($query) use ($ticket, $linkedTicket): void {
|
||||||
$query
|
$query
|
||||||
|
|||||||
@@ -146,6 +146,66 @@ class ProjectTicketLinkControllerTest extends TestCase
|
|||||||
->assertSessionHasErrors('linked_ticket_id');
|
->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.
|
* Ensure users with edit permission can remove ticket links.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user