Model ticket link counterparts explicitly
linter / quality (pull_request) Successful in 3m5s
tests / ci (8.4) (pull_request) Failing after 1m41s

This commit is contained in:
2026-05-23 10:29:22 +00:00
parent d60c33fa7c
commit 486994e441
22 changed files with 312 additions and 66 deletions
+4 -4
View File
@@ -20,9 +20,9 @@ class TicketLinkTest extends TestCase
use RefreshDatabase;
/**
* Ensure the ticket link factory normalizes the stored source and target ticket ids.
* Ensure the ticket link factory preserves the provided source and target direction.
*/
public function test_ticket_link_factory_normalizes_ticket_order(): void
public function test_ticket_link_factory_preserves_ticket_direction(): void
{
$project = Project::factory()->create([
'key' => 'CORE',
@@ -61,8 +61,8 @@ class TicketLinkTest extends TestCase
'ticket_link_type_id' => $ticketLinkType->id,
]);
$this->assertSame($firstTicket->id, $ticketLink->source_ticket_id);
$this->assertSame($secondTicket->id, $ticketLink->target_ticket_id);
$this->assertSame($secondTicket->id, $ticketLink->source_ticket_id);
$this->assertSame($firstTicket->id, $ticketLink->target_ticket_id);
}
/**
@@ -22,6 +22,7 @@ class TicketLinkTypeTest extends TestCase
$this->assertNotNull($ticketLinkType->project_id);
$this->assertIsString($ticketLinkType->label);
$this->assertSame($ticketLinkType->label, $ticketLinkType->counterpart_label);
}
/**
@@ -39,12 +40,14 @@ class TicketLinkTypeTest extends TestCase
$this->assertDatabaseHas('ticket_link_types', [
'project_id' => $coreProject->id,
'label' => 'Relates to',
'counterpart_label' => 'Relates to',
'position' => 1,
]);
$this->assertDatabaseHas('ticket_link_types', [
'project_id' => $coreProject->id,
'label' => 'Blocks',
'counterpart_label' => 'Blocked by',
'position' => 2,
]);
}
@@ -55,6 +55,7 @@ class ProjectTicketControllerTest extends TestCase
]);
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
'label' => 'Relates to',
'counterpart_label' => 'Relates to',
'position' => 1,
]);
@@ -137,6 +138,7 @@ class ProjectTicketControllerTest extends TestCase
->where('ticket.worklog_entries.0.actor.name', $viewer->name)
->where('linkedTickets.0.linked_ticket.ticket_key', 'CORE-2')
->where('linkedTickets.0.ticket_link_type.label', 'Relates to')
->where('linkedTickets.0.ticket_link_type.counterpart_label', 'Relates to')
->where('repositoryBranches', [])
->where('repositoryPullRequests', [])
->where('canEditTicket', false)
@@ -144,6 +146,62 @@ class ProjectTicketControllerTest extends TestCase
);
}
/**
* Ensure linked tickets show the counterpart label from the other ticket perspective.
*/
public function test_ticket_detail_uses_counterpart_link_label_for_target_ticket(): void
{
$viewer = 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();
$sourceTicket = 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' => 'Source ticket',
]);
$targetTicket = Ticket::factory()->create([
'project_id' => $project->id,
'sequence_number' => 2,
'ticket_key' => 'CORE-2',
'ticket_priority_id' => $priority->id,
'ticket_type_id' => $taskType->id,
'title' => 'Target ticket',
]);
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
'label' => 'Blocks',
'counterpart_label' => 'Blocked by',
'position' => 1,
]);
TicketLink::factory()->create([
'source_ticket_id' => $sourceTicket->id,
'target_ticket_id' => $targetTicket->id,
'ticket_link_type_id' => $ticketLinkType->id,
]);
$this->syncProjectPermissions($project, $viewer, ['view']);
$this->actingAs($viewer)
->get(route('projects.tickets.show', [$project, $targetTicket]))
->assertOk()
->assertInertia(fn (Assert $page) => $page
->where('linkedTickets.0.linked_ticket.ticket_key', 'CORE-1')
->where('linkedTickets.0.ticket_link_type.label', 'Blocked by')
->where('linkedTickets.0.ticket_link_type.counterpart_label', 'Blocked by'));
}
/**
* Ensure project admins can still inspect the author when project author visibility is disabled.
*/
@@ -33,7 +33,8 @@ class ProjectTicketLinkControllerTest extends TestCase
->orderBy('position')
->firstOrFail();
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
'label' => 'Relates to',
'label' => 'Blocks',
'counterpart_label' => 'Blocked by',
'position' => 1,
]);
$ticket = Ticket::factory()->create([
@@ -59,11 +60,11 @@ class ProjectTicketLinkControllerTest extends TestCase
'ticket_link_type_id' => $ticketLinkType->id,
])
->assertRedirect(route('projects.tickets.show', [$project, $ticket]))
->assertSessionHas('status', 'Relates to link was saved.');
->assertSessionHas('status', 'Blocks link was saved.');
$this->assertDatabaseHas('ticket_links', [
'source_ticket_id' => min($ticket->id, $linkedTicket->id),
'target_ticket_id' => max($ticket->id, $linkedTicket->id),
'source_ticket_id' => $ticket->id,
'target_ticket_id' => $linkedTicket->id,
'ticket_link_type_id' => $ticketLinkType->id,
]);
@@ -71,7 +72,14 @@ class ProjectTicketLinkControllerTest extends TestCase
'ticket_id' => $ticket->id,
'actor_id' => $user->id,
'entry_type' => 'link_updated',
'summary' => 'Linked CORE-2 as Relates to.',
'summary' => 'Linked CORE-2 as Blocks.',
]);
$this->assertDatabaseHas('ticket_worklog_entries', [
'ticket_id' => $linkedTicket->id,
'actor_id' => $user->id,
'entry_type' => 'link_updated',
'summary' => 'Linked CORE-1 as Blocked by.',
]);
}
@@ -99,6 +107,7 @@ class ProjectTicketLinkControllerTest extends TestCase
->firstOrFail();
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
'label' => 'Relates to',
'counterpart_label' => 'Relates to',
'position' => 1,
]);
$ticket = Ticket::factory()->create([
@@ -155,7 +164,8 @@ class ProjectTicketLinkControllerTest extends TestCase
->orderBy('position')
->firstOrFail();
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
'label' => 'Relates to',
'label' => 'Blocks',
'counterpart_label' => 'Blocked by',
'position' => 1,
]);
$ticket = Ticket::factory()->create([
@@ -173,8 +183,8 @@ class ProjectTicketLinkControllerTest extends TestCase
'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),
'source_ticket_id' => $ticket->id,
'target_ticket_id' => $linkedTicket->id,
'ticket_link_type_id' => $ticketLinkType->id,
]);
@@ -183,7 +193,7 @@ class ProjectTicketLinkControllerTest extends TestCase
$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.');
->assertSessionHas('status', 'Blocks link was removed.');
$this->assertDatabaseMissing('ticket_links', [
'id' => $ticketLink->id,
@@ -193,7 +203,14 @@ class ProjectTicketLinkControllerTest extends TestCase
'ticket_id' => $ticket->id,
'actor_id' => $user->id,
'entry_type' => 'link_removed',
'summary' => 'Removed Relates to link to CORE-2.',
'summary' => 'Removed Blocks link to CORE-2.',
]);
$this->assertDatabaseHas('ticket_worklog_entries', [
'ticket_id' => $linkedTicket->id,
'actor_id' => $user->id,
'entry_type' => 'link_removed',
'summary' => 'Removed Blocked by link to CORE-1.',
]);
}
}
@@ -420,6 +420,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$project = Project::factory()->create();
$ticketLinkType = TicketLinkType::factory()->for($project)->create([
'label' => 'Relates to',
'counterpart_label' => 'Relates to',
'position' => 1,
]);
@@ -428,18 +429,32 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$this->actingAs($user)
->post(route('projects.settings.metadata.link-types.store', $project), [
'name' => 'Blocks',
'counterpart_name' => 'Blocked by',
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'Blocks link type was added.');
$this->assertDatabaseHas('ticket_link_types', [
'project_id' => $project->id,
'label' => 'Blocks',
'counterpart_label' => 'Blocked by',
]);
$this->actingAs($user)
->patch(route('projects.settings.metadata.link-types.update', [$project, $ticketLinkType]), [
'name' => 'Depends on',
'counterpart_name' => 'Required by',
'position' => 2,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'Depends on link type was updated.');
$this->assertDatabaseHas('ticket_link_types', [
'id' => $ticketLinkType->id,
'label' => 'Depends on',
'counterpart_label' => 'Required by',
]);
$this->actingAs($user)
->delete(route('projects.settings.metadata.link-types.destroy', [$project, $ticketLinkType]))
->assertRedirect(route('projects.settings.metadata', $project))