create(); $project = Project::factory()->create([ 'key' => 'AUTH', ]); app(InitializeProjectTicketMetadata::class)->handle($project); BoardColumn::factory()->for($project)->create([ 'name' => 'Todo', 'position' => 1, ]); $priority = $project->ticketPriorities()->firstOrFail(); $backlogStatus = $project->ticketStatuses() ->whereNull('board_column_id') ->firstOrFail(); $ticket = Ticket::factory()->create([ 'project_id' => $project->id, 'ticket_priority_id' => $priority->id, 'ticket_status_id' => $backlogStatus->id, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'ticket_key' => 'AUTH-1', 'sequence_number' => 1, 'title' => 'Seed backlog item', ]); $this->syncProjectPermissions($project, $user, ['view']); $this->actingAs($user) ->get(route('projects.backlog.index', $project)) ->assertOk() ->assertInertia(fn (Assert $page) => $page ->component('projects/Backlog') ->where('backlogTicketStatuses.0.name', 'Backlog') ->where('canEditTickets', false) ->where('project.id', $project->id) ->where('tickets.0.ticket_key', $ticket->ticket_key) ->where('canCreateTickets', false), ); } /** * Ensure creating tickets requires the project write permission. */ public function test_creating_tickets_requires_project_write_permission(): void { $user = User::factory()->create(); $project = Project::factory()->create(); app(InitializeProjectTicketMetadata::class)->handle($project); $priority = $project->ticketPriorities()->firstOrFail(); $backlogStatus = $project->ticketStatuses() ->whereNull('board_column_id') ->firstOrFail(); $this->syncProjectPermissions($project, $user, ['view']); $this->actingAs($user) ->post(route('projects.tickets.store', $project), [ 'title' => 'New ticket', 'description' => 'Description', 'acceptance_criteria' => 'Criteria', 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'ticket_priority_id' => $priority->id, 'ticket_status_id' => $backlogStatus->id, ]) ->assertForbidden(); } /** * Ensure project-local keys increment and ticket creation accepts duplicate titles. */ public function test_users_with_write_permission_can_add_duplicate_titled_tickets(): void { $user = User::factory()->create(); $assignee = User::factory()->create(); $project = Project::factory()->create([ 'key' => 'AUTH', ]); app(InitializeProjectTicketMetadata::class)->handle($project); $priority = TicketPriority::factory()->for($project)->create([ 'name' => 'High', 'position' => 2, ]); $backlogStatus = $project->ticketStatuses() ->whereNull('board_column_id') ->firstOrFail(); $this->syncProjectPermissions($project, $user, ['create', 'view']); $payload = [ 'title' => 'Same title', 'description' => 'Description', 'acceptance_criteria' => 'Criteria', 'assignee_user_id' => $assignee->id, 'story_points' => 3, 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'ticket_priority_id' => $priority->id, 'ticket_status_id' => $backlogStatus->id, ]; $this->actingAs($user) ->post(route('projects.tickets.store', $project), $payload) ->assertRedirect(route('projects.tickets.show', [$project, 1])) ->assertSessionHas('status', 'AUTH-1 was created.'); $this->actingAs($user) ->post(route('projects.tickets.store', $project), $payload) ->assertRedirect(route('projects.tickets.show', [$project, 2])) ->assertSessionHas('status', 'AUTH-2 was created.'); $this->assertDatabaseHas('tickets', [ 'project_id' => $project->id, 'ticket_key' => 'AUTH-1', 'title' => 'Same title', 'assignee_user_id' => $assignee->id, 'author_user_id' => $user->id, 'story_points' => 3, 'ticket_status_id' => $backlogStatus->id, ]); $this->assertDatabaseHas('tickets', [ 'project_id' => $project->id, 'ticket_key' => 'AUTH-2', 'title' => 'Same title', 'assignee_user_id' => $assignee->id, 'author_user_id' => $user->id, 'story_points' => 3, 'ticket_status_id' => $backlogStatus->id, ]); $this->assertDatabaseCount('ticket_worklog_entries', 2); $this->assertDatabaseHas('ticket_worklog_entries', [ 'entry_type' => 'created', 'summary' => 'Ticket was created in Backlog.', ]); } /** * Ensure ticket creation can target board column statuses. */ public function test_creating_tickets_accepts_board_column_statuses(): void { $user = User::factory()->create(); $project = Project::factory()->create(); app(InitializeProjectTicketMetadata::class)->handle($project); $boardColumn = BoardColumn::factory()->for($project)->create([ 'name' => 'Todo', 'position' => 1, ]); $boardStatus = TicketStatus::factory() ->for($project) ->for($boardColumn) ->create([ 'name' => 'Todo', 'position' => 1, ]); $this->syncProjectPermissions($project, $user, ['create', 'view']); $this->actingAs($user) ->from(route('projects.backlog.index', $project)) ->post(route('projects.tickets.store', $project), [ 'title' => 'New ticket', 'description' => 'Description', 'acceptance_criteria' => 'Criteria', 'ticket_type_id' => $project->ticketTypes()->firstOrFail()->id, 'ticket_priority_id' => $project->ticketPriorities()->firstOrFail()->id, 'ticket_status_id' => $boardStatus->id, ]) ->assertRedirect(route('projects.tickets.show', [$project, 1])) ->assertSessionHas('status', "{$project->key}-1 was created."); $this->assertDatabaseHas('tickets', [ 'project_id' => $project->id, 'ticket_status_id' => $boardStatus->id, 'title' => 'New ticket', ]); } }