artisan('db:seed', ['--class' => 'RoleSeeder']); } public function test_issue_number_auto_generation(): void { $issue1 = Issue::factory()->create(); $issue2 = Issue::factory()->create(); $this->assertMatchesRegularExpression('/^ISS-\d{4}-\d{3}$/', $issue1->issue_number); $this->assertMatchesRegularExpression('/^ISS-\d{4}-\d{3}$/', $issue2->issue_number); $this->assertNotEquals($issue1->issue_number, $issue2->issue_number); } public function test_issue_belongs_to_creator(): void { $creator = User::factory()->create(); $issue = Issue::factory()->create(['created_by_user_id' => $creator->id]); $this->assertInstanceOf(User::class, $issue->creator); $this->assertEquals($creator->id, $issue->creator->id); } public function test_issue_belongs_to_assignee(): void { $assignee = User::factory()->create(); $issue = Issue::factory()->create(['assigned_to_user_id' => $assignee->id]); $this->assertInstanceOf(User::class, $issue->assignee); $this->assertEquals($assignee->id, $issue->assignee->id); } public function test_issue_belongs_to_reviewer(): void { $reviewer = User::factory()->create(); $issue = Issue::factory()->create(['reviewer_id' => $reviewer->id]); $this->assertInstanceOf(User::class, $issue->reviewer); $this->assertEquals($reviewer->id, $issue->reviewer->id); } public function test_issue_has_many_comments(): void { $issue = Issue::factory()->create(); $comment1 = IssueComment::factory()->create(['issue_id' => $issue->id]); $comment2 = IssueComment::factory()->create(['issue_id' => $issue->id]); $this->assertCount(2, $issue->comments); $this->assertTrue($issue->comments->contains($comment1)); } public function test_issue_has_many_attachments(): void { $issue = Issue::factory()->create(); $attachment1 = IssueAttachment::factory()->create(['issue_id' => $issue->id]); $attachment2 = IssueAttachment::factory()->create(['issue_id' => $issue->id]); $this->assertCount(2, $issue->attachments); } public function test_issue_has_many_time_logs(): void { $issue = Issue::factory()->create(); $log1 = IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 2.5]); $log2 = IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 3.5]); $this->assertCount(2, $issue->timeLogs); } public function test_issue_has_many_labels(): void { $issue = Issue::factory()->create(); $label1 = IssueLabel::factory()->create(); $label2 = IssueLabel::factory()->create(); $issue->labels()->attach([$label1->id, $label2->id]); $this->assertCount(2, $issue->labels); $this->assertTrue($issue->labels->contains($label1)); } public function test_issue_has_many_watchers(): void { $issue = Issue::factory()->create(); $watcher1 = User::factory()->create(); $watcher2 = User::factory()->create(); $issue->watchers()->attach([$watcher1->id, $watcher2->id]); $this->assertCount(2, $issue->watchers); } public function test_status_check_methods_work(): void { $issue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertTrue($issue->isNew()); $this->assertFalse($issue->isAssigned()); $issue->status = Issue::STATUS_ASSIGNED; $this->assertTrue($issue->isAssigned()); $this->assertFalse($issue->isNew()); $issue->status = Issue::STATUS_IN_PROGRESS; $this->assertTrue($issue->isInProgress()); $issue->status = Issue::STATUS_REVIEW; $this->assertTrue($issue->inReview()); $issue->status = Issue::STATUS_CLOSED; $this->assertTrue($issue->isClosed()); $this->assertFalse($issue->isOpen()); } public function test_can_be_assigned_validates_correctly(): void { $newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertTrue($newIssue->canBeAssigned()); $closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]); $this->assertFalse($closedIssue->canBeAssigned()); } public function test_can_move_to_in_progress_validates_correctly(): void { $user = User::factory()->create(); $assignedIssue = Issue::factory()->create([ 'status' => Issue::STATUS_ASSIGNED, 'assigned_to_user_id' => $user->id, ]); $this->assertTrue($assignedIssue->canMoveToInProgress()); $newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertFalse($newIssue->canMoveToInProgress()); $assignedWithoutUser = Issue::factory()->create([ 'status' => Issue::STATUS_ASSIGNED, 'assigned_to_user_id' => null, ]); $this->assertFalse($assignedWithoutUser->canMoveToInProgress()); } public function test_can_move_to_review_validates_correctly(): void { $inProgressIssue = Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]); $this->assertTrue($inProgressIssue->canMoveToReview()); $newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertFalse($newIssue->canMoveToReview()); } public function test_can_be_closed_validates_correctly(): void { $reviewIssue = Issue::factory()->create(['status' => Issue::STATUS_REVIEW]); $this->assertTrue($reviewIssue->canBeClosed()); $inProgressIssue = Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]); $this->assertTrue($inProgressIssue->canBeClosed()); $newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertFalse($newIssue->canBeClosed()); } public function test_can_be_reopened_validates_correctly(): void { $closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]); $this->assertTrue($closedIssue->canBeReopened()); $openIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertFalse($openIssue->canBeReopened()); } public function test_progress_percentage_calculation(): void { $newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertEquals(0, $newIssue->progress_percentage); $assignedIssue = Issue::factory()->create(['status' => Issue::STATUS_ASSIGNED]); $this->assertEquals(25, $assignedIssue->progress_percentage); $inProgressIssue = Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]); $this->assertEquals(50, $inProgressIssue->progress_percentage); $reviewIssue = Issue::factory()->create(['status' => Issue::STATUS_REVIEW]); $this->assertEquals(75, $reviewIssue->progress_percentage); $closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]); $this->assertEquals(100, $closedIssue->progress_percentage); } public function test_overdue_detection_works(): void { $overdueIssue = Issue::factory()->create([ 'due_date' => now()->subDays(5), 'status' => Issue::STATUS_IN_PROGRESS, ]); $this->assertTrue($overdueIssue->is_overdue); $upcomingIssue = Issue::factory()->create([ 'due_date' => now()->addDays(5), 'status' => Issue::STATUS_IN_PROGRESS, ]); $this->assertFalse($upcomingIssue->is_overdue); $closedOverdueIssue = Issue::factory()->create([ 'due_date' => now()->subDays(5), 'status' => Issue::STATUS_CLOSED, ]); $this->assertFalse($closedOverdueIssue->is_overdue); } public function test_days_until_due_calculation(): void { $issue = Issue::factory()->create([ 'due_date' => now()->addDays(5), ]); $this->assertEquals(5, $issue->days_until_due); $overdueIssue = Issue::factory()->create([ 'due_date' => now()->subDays(3), ]); $this->assertEquals(-3, $overdueIssue->days_until_due); } public function test_total_time_logged_calculation(): void { $issue = Issue::factory()->create(); IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 2.5]); IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 3.5]); IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 1.0]); $this->assertEquals(7.0, $issue->total_time_logged); } public function test_status_label_returns_correct_text(): void { $issue = Issue::factory()->create(['status' => Issue::STATUS_NEW]); $this->assertEquals('New', $issue->status_label); $issue->status = Issue::STATUS_CLOSED; $this->assertEquals('Closed', $issue->status_label); } public function test_priority_label_returns_correct_text(): void { $issue = Issue::factory()->create(['priority' => Issue::PRIORITY_LOW]); $this->assertEquals('Low', $issue->priority_label); $issue->priority = Issue::PRIORITY_URGENT; $this->assertEquals('Urgent', $issue->priority_label); } public function test_badge_color_methods_work(): void { $urgentIssue = Issue::factory()->create(['priority' => Issue::PRIORITY_URGENT]); $this->assertStringContainsString('red', $urgentIssue->priority_badge_color); $closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]); $this->assertStringContainsString('gray', $closedIssue->status_badge_color); } public function test_scopes_work(): void { Issue::factory()->create(['status' => Issue::STATUS_NEW]); Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]); Issue::factory()->create(['status' => Issue::STATUS_CLOSED]); $openIssues = Issue::open()->get(); $this->assertCount(2, $openIssues); $closedIssues = Issue::closed()->get(); $this->assertCount(1, $closedIssues); } public function test_overdue_scope_works(): void { Issue::factory()->create([ 'due_date' => now()->subDays(5), 'status' => Issue::STATUS_IN_PROGRESS, ]); Issue::factory()->create([ 'due_date' => now()->addDays(5), 'status' => Issue::STATUS_IN_PROGRESS, ]); $overdueIssues = Issue::overdue()->get(); $this->assertCount(1, $overdueIssues); } public function test_parent_child_relationships_work(): void { $parentIssue = Issue::factory()->create(); $childIssue1 = Issue::factory()->create(['parent_issue_id' => $parentIssue->id]); $childIssue2 = Issue::factory()->create(['parent_issue_id' => $parentIssue->id]); $this->assertCount(2, $parentIssue->subTasks); $this->assertInstanceOf(Issue::class, $childIssue1->parentIssue); $this->assertEquals($parentIssue->id, $childIssue1->parentIssue->id); } public function test_issue_type_label_returns_correct_text(): void { $issue = Issue::factory()->create(['issue_type' => Issue::TYPE_WORK_ITEM]); $this->assertEquals('Work Item', $issue->issue_type_label); $issue->issue_type = Issue::TYPE_MEMBER_REQUEST; $this->assertEquals('Member Request', $issue->issue_type_label); } }