codex ticket rework

This commit is contained in:
2026-04-13 01:04:06 +02:00
parent b3f2cc3afd
commit 182ed018d1
45 changed files with 1208 additions and 420 deletions
@@ -2,7 +2,9 @@
namespace Tests\Feature\Projects;
use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata;
use App\Models\Project;
use App\Models\Ticket;
use App\Models\TicketFieldDefinition;
use App\Models\TicketLinkType;
use App\Models\TicketPriority;
@@ -23,6 +25,9 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
{
$user = User::factory()->create();
$project = Project::factory()->create();
app(InitializeProjectTicketMetadata::class)->handle($project);
TicketType::factory()->for($project)->create([
'name' => 'Bug',
'position' => 3,
@@ -39,8 +44,11 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('projects/settings/Metadata')
->where('project.show_author', true)
->where('project.show_story_points', true)
->where('ticketTypes.0.name', 'Task')
->where('ticketTypes.1.name', 'Parent')
->where('ticketTypes.1.name', 'Release')
->where('ticketTypes.1.is_parent', true)
->where('ticketTypes.2.name', 'Bug')
->where('ticketLinkTypes.0.label', 'Relates to'),
);
@@ -58,6 +66,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$this->actingAs($user)
->post(route('projects.settings.metadata.types.store', $project), [
'is_parent' => '1',
'name' => 'Bug',
])
->assertRedirect(route('projects.settings.metadata', $project))
@@ -66,57 +75,245 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$this->assertDatabaseHas('ticket_types', [
'project_id' => $project->id,
'name' => 'Bug',
'is_builtin' => false,
'system_key' => null,
'is_parent' => true,
]);
}
/**
* Ensure built-in ticket types can be renamed but not deleted.
* Ensure type parent flags cannot be changed after creation.
*/
public function test_builtin_ticket_types_can_be_renamed_but_not_deleted(): void
public function test_ticket_type_parent_flags_cannot_be_changed_after_creation(): void
{
$user = User::factory()->create();
$project = Project::factory()->create();
app(InitializeProjectTicketMetadata::class)->handle($project);
$releaseType = $project->ticketTypes()
->where('is_parent', true)
->orderBy('position')
->firstOrFail();
$this->syncProjectPermissions($project, $user, ['edit']);
$taskType = TicketType::query()
->where('project_id', $project->id)
->where('system_key', 'task')
->first();
if ($taskType === null) {
$this->actingAs($user)
->get(route('projects.settings.metadata', $project))
->assertOk();
$taskType = TicketType::query()
->where('project_id', $project->id)
->where('system_key', 'task')
->firstOrFail();
}
$this->actingAs($user)
->patch(route('projects.settings.metadata.types.update', [$project, $taskType]), [
'name' => 'Standard Task',
'position' => 1,
->patch(route('projects.settings.metadata.types.update', [$project, $releaseType]), [
'is_parent' => '0',
'name' => 'Standard Release',
'position' => 2,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'Standard Task type was updated.');
->assertSessionHas('status', 'Standard Release type was updated.');
$this->assertDatabaseHas('ticket_types', [
'id' => $taskType->id,
'name' => 'Standard Task',
'system_key' => 'task',
'is_builtin' => true,
'id' => $releaseType->id,
'name' => 'Standard Release',
'is_parent' => true,
]);
}
/**
* Ensure users with edit permission can update project ticket visibility settings.
*/
public function test_users_with_edit_permission_can_update_ticket_visibility_settings(): void
{
$user = User::factory()->create();
$project = Project::factory()->create([
'show_author' => true,
'show_story_points' => true,
]);
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->delete(route('projects.settings.metadata.types.destroy', [$project, $taskType]))
->patch(route('projects.settings.metadata.update', $project), [
'show_author' => false,
'show_story_points' => false,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'Settings have been updated.');
$this->assertDatabaseHas('projects', [
'id' => $project->id,
'show_author' => false,
'show_story_points' => false,
]);
}
/**
* Ensure users without project admin permission cannot update ticket metadata settings.
*/
public function test_non_admin_users_cannot_update_ticket_metadata_settings(): void
{
$user = User::factory()->create();
$project = Project::factory()->create();
$this->syncProjectPermissions($project, $user, ['view']);
$this->actingAs($user)
->patch(route('projects.settings.metadata.update', $project), [
'show_author' => false,
'show_story_points' => false,
])
->assertForbidden();
}
/**
* Ensure the last remaining ticket type cannot be deleted.
*/
public function test_the_last_remaining_ticket_type_cannot_be_deleted(): void
{
$user = User::factory()->create();
$project = Project::factory()->create();
$ticketType = TicketType::factory()->for($project)->create([
'name' => 'Solo',
'position' => 1,
]);
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->delete(route('projects.settings.metadata.types.destroy', [$project, $ticketType]))
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'The last ticket type cannot be deleted.');
$this->assertDatabaseHas('ticket_types', [
'id' => $ticketType->id,
]);
}
/**
* Ensure deleting a used ticket type requires a replacement and reassigns affected tickets.
*/
public function test_used_ticket_types_require_replacement_before_deletion_and_reassign_tickets(): void
{
$user = User::factory()->create();
$author = 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();
$releaseType = $project->ticketTypes()
->where('is_parent', true)
->orderBy('position')
->firstOrFail();
$ticket = Ticket::factory()->create([
'author_user_id' => $author->id,
'project_id' => $project->id,
'sequence_number' => 1,
'ticket_key' => 'CORE-1',
'ticket_priority_id' => $priority->id,
'ticket_type_id' => $taskType->id,
'title' => 'Replace my type',
]);
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->from(route('projects.settings.metadata', $project))
->delete(route('projects.settings.metadata.types.destroy', [$project, $taskType]))
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHasErrors('replacement_ticket_type_id');
$this->actingAs($user)
->delete(route('projects.settings.metadata.types.destroy', [$project, $taskType]), [
'replacement_ticket_type_id' => $releaseType->id,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'Task type was removed.');
$this->assertDatabaseMissing('ticket_types', [
'id' => $taskType->id,
]);
$this->assertDatabaseHas('tickets', [
'id' => $ticket->id,
'ticket_type_id' => $releaseType->id,
]);
$this->assertDatabaseHas('ticket_worklog_entries', [
'ticket_id' => $ticket->id,
'actor_id' => $user->id,
'entry_type' => 'updated',
'summary' => 'Updated type because Task type was deleted.',
]);
}
/**
* Ensure parent tickets cannot be reassigned to a non-parent replacement type during delete.
*/
public function test_parent_tickets_require_a_parent_enabled_replacement_type_when_deleting_types(): void
{
$user = User::factory()->create();
$author = 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();
$releaseType = $project->ticketTypes()
->where('is_parent', true)
->orderBy('position')
->firstOrFail();
$epicType = TicketType::factory()->for($project)->create([
'is_parent' => true,
'name' => 'Epic',
'position' => 3,
]);
$parentTicket = Ticket::factory()->create([
'author_user_id' => $author->id,
'project_id' => $project->id,
'sequence_number' => 1,
'ticket_key' => 'CORE-1',
'ticket_priority_id' => $priority->id,
'ticket_type_id' => $releaseType->id,
'title' => 'Parent ticket',
]);
Ticket::factory()->create([
'author_user_id' => $author->id,
'parent_ticket_id' => $parentTicket->id,
'project_id' => $project->id,
'sequence_number' => 2,
'ticket_key' => 'CORE-2',
'ticket_priority_id' => $priority->id,
'ticket_type_id' => $taskType->id,
'title' => 'Child ticket',
]);
$this->syncProjectPermissions($project, $user, ['edit']);
$this->actingAs($user)
->from(route('projects.settings.metadata', $project))
->delete(route('projects.settings.metadata.types.destroy', [$project, $releaseType]), [
'replacement_ticket_type_id' => $taskType->id,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHasErrors('replacement_ticket_type_id');
$this->actingAs($user)
->delete(route('projects.settings.metadata.types.destroy', [$project, $releaseType]), [
'replacement_ticket_type_id' => $epicType->id,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'Release type was removed.');
$this->assertDatabaseHas('tickets', [
'id' => $parentTicket->id,
'ticket_type_id' => $epicType->id,
]);
}
/**
* Ensure priorities can be created, updated, and deleted.
*/
@@ -163,7 +360,7 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$user = User::factory()->create();
$project = Project::factory()->create();
$fieldDefinition = TicketFieldDefinition::factory()->for($project)->create([
'label' => 'Story Points',
'label' => 'Complexity',
'field_type' => 'number',
'position' => 1,
]);
@@ -180,16 +377,16 @@ class ProjectTicketMetadataSettingsControllerTest extends TestCase
$this->actingAs($user)
->patch(route('projects.settings.metadata.fields.update', [$project, $fieldDefinition]), [
'label' => 'Estimated Points',
'label' => 'Risk score',
'field_type' => 'number',
'position' => 2,
])
->assertRedirect(route('projects.settings.metadata', $project))
->assertSessionHas('status', 'Estimated Points field was updated.');
->assertSessionHas('status', 'Risk score field was updated.');
$this->assertDatabaseHas('ticket_field_definitions', [
'id' => $fieldDefinition->id,
'label' => 'Estimated Points',
'label' => 'Risk score',
'field_type' => 'number',
]);
}