create(); $this->assertNull($ticket->board_column_id); $this->assertStringContainsString('-', $ticket->ticket_key); $this->assertSame(1, $ticket->sequence_number); } /** * Ensure the ticket seeder creates duplicate titles with project-local keys. */ public function test_ticket_seeder_creates_backlog_tickets_with_project_local_keys(): void { $this->seed([ ProjectSeeder::class, BoardColumnSeeder::class, ProjectTicketMetadataSeeder::class, TicketSeeder::class, ]); $coreProject = Project::query()->where('key', 'CORE')->firstOrFail(); $this->assertDatabaseHas('tickets', [ 'project_id' => $coreProject->id, 'ticket_key' => 'CORE-1', 'title' => 'Seed backlog item', 'board_column_id' => null, ]); $this->assertDatabaseHas('tickets', [ 'project_id' => $coreProject->id, 'ticket_key' => 'CORE-2', 'title' => 'Seed backlog item', 'board_column_id' => null, ]); } /** * Ensure ticket worklog entries are exposed newest first for timeline rendering. */ public function test_ticket_worklog_entries_are_returned_newest_first(): void { $ticket = Ticket::factory()->create(); $olderEntry = TicketWorklogEntry::factory()->for($ticket)->create([ 'created_at' => now()->subMinute(), 'updated_at' => now()->subMinute(), ]); $newerEntry = TicketWorklogEntry::factory()->for($ticket)->create([ 'created_at' => now(), 'updated_at' => now(), ]); $this->assertSame( [$newerEntry->id, $olderEntry->id], $ticket->fresh()->worklogEntries->modelKeys(), ); } /** * Ensure ticket field values are available through the ticket relation. */ public function test_ticket_field_values_are_available_through_the_ticket_relation(): void { $project = Project::factory()->create([ 'key' => 'CORE', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'Medium', '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' => $project->ticketTypes()->firstOrFail()->id, ]); $fieldDefinition = TicketFieldDefinition::factory()->for($project)->create([ 'field_type' => 'number', 'label' => 'Story Points', 'position' => 1, ]); $fieldValue = TicketFieldValue::factory()->create([ 'ticket_field_definition_id' => $fieldDefinition->id, 'ticket_id' => $ticket->id, 'value_number' => 5, 'value_user_id' => null, ]); $this->assertSame( [$fieldValue->id], $ticket->fresh()->fieldValues->modelKeys(), ); } /** * Ensure tickets expose parent and child relationships for hierarchy rendering. */ public function test_ticket_parent_and_child_relationships_are_available(): void { $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(); $parentType = $project->ticketTypes()->where('system_key', 'parent')->firstOrFail(); $parentTicket = Ticket::factory()->create([ 'project_id' => $project->id, 'sequence_number' => 1, 'ticket_key' => 'CORE-1', 'ticket_priority_id' => $priority->id, 'ticket_type_id' => $parentType->id, ]); $childTicket = Ticket::factory()->create([ '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, ]); $this->assertSame($parentTicket->id, $childTicket->fresh()->parentTicket?->id); $this->assertSame([$childTicket->id], $parentTicket->fresh()->childTickets->modelKeys()); } /** * Ensure the blocked flag is cast to a boolean for board and detail rendering. */ public function test_ticket_blocked_state_is_cast_to_boolean(): void { $ticket = Ticket::factory()->create([ 'blocked_comment' => 'Waiting for environment access.', 'is_blocked' => 1, ]); $freshTicket = $ticket->fresh(); $this->assertTrue($freshTicket->is_blocked); $this->assertSame('Waiting for environment access.', $freshTicket->blocked_comment); } /** * Ensure tickets expose incoming and outgoing link relations. */ public function test_ticket_link_relations_are_available(): void { $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(); $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, ]); $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, ]); $ticketLinkType = TicketLinkType::factory()->for($project)->create([ 'label' => 'Relates to', 'position' => 1, ]); $ticketLink = TicketLink::factory()->create([ 'source_ticket_id' => $sourceTicket->id, 'target_ticket_id' => $targetTicket->id, 'ticket_link_type_id' => $ticketLinkType->id, ]); $this->assertSame( [$ticketLink->id], $sourceTicket->fresh()->ticketLinksAsSource->modelKeys(), ); $this->assertSame( [$ticketLink->id], $targetTicket->fresh()->ticketLinksAsTarget->modelKeys(), ); } }