seed(DatabaseSeeder::class); }); test('guests cannot access the tickets page', function (): void { $this->get(route('tickets.index')) ->assertRedirect(route('login')); }); test('authenticated users with correct permissions can access the tickets page', function (): void { $user = User::factory()->create(); $user->assignRole('user'); $this->actingAs($user); $this->get(route('tickets.index')) ->assertOk() ->assertSee('Support Tickets') ->assertSee('You do not currently have any active services expiring within 3 days'); }); test('submitting a support ticket creates the ticket and generates a unique ticket ID', function (): void { $user = User::factory()->create(); $user->assignRole('user'); $this->actingAs($user); $app = ConnectedApp::first(); $supportRole = Role::where('name', 'Support')->firstOrFail(); $dto = new CreateTicketRequest( userId: $user->id, connectedAppId: $app->id, issueCategory: 'Billing Query', description: 'Need assistance with billing renewal.', notifiedRoleIds: [$supportRole->id], notifiedUserIds: [], attachment: null, ); $service = app(TicketService::class); $ticketData = $service->create($dto); expect($ticketData->ticketId)->toStartWith('TKT-') ->and(mb_strlen($ticketData->ticketId))->toBe(12) ->and($ticketData->issueCategory)->toBe('Billing Query') ->and($ticketData->status->value)->toBe(TicketStatusEnum::Open->value) ->and($ticketData->notifiedRoleNames)->toContain('Support'); $this->assertDatabaseHas('tickets', [ 'user_id' => $user->id, 'connected_app_id' => $app->id, 'issue_category' => 'Billing Query', ]); $this->assertDatabaseHas('ticket_notified_roles', [ 'ticket_id' => $ticketData->id, 'role_id' => $supportRole->id, ]); }); test('submitting a ticket with category Others requires at least 20 characters for description', function (): void { $user = User::factory()->create(); $user->assignRole('user'); $this->actingAs($user); $supportRole = Role::where('name', 'Support')->firstOrFail(); // Test Validation Failure via Livewire component form object Livewire::test('pages::tickets.index') ->set('form.issueCategory', 'Others') ->set('form.description', 'Short desc') ->set('form.routingType', 'role') ->set('form.notifiedRoleIds', [$supportRole->id]) ->call('saveTicket') ->assertHasErrors(['form.description' => 'min']); }); test('attaching a file for Others category uploads and stores the file', function (): void { Storage::fake('public'); $user = User::factory()->create(); $user->assignRole('user'); $this->actingAs($user); $supportRole = Role::where('name', 'Support')->firstOrFail(); $file = UploadedFile::fake()->create('screenshot.png', 500); $dto = new CreateTicketRequest( userId: $user->id, connectedAppId: null, issueCategory: 'Others', description: 'A very detailed query containing more than twenty characters.', notifiedRoleIds: [$supportRole->id], notifiedUserIds: [], attachment: $file, ); $service = app(TicketService::class); $ticketData = $service->create($dto); $this->assertDatabaseHas('ticket_attachments', [ 'file_name' => 'screenshot.png', 'mime_type' => 'image/png', ]); expect(count($ticketData->attachments))->toBe(1); Storage::disk('public')->assertExists($ticketData->attachments[0]['file_path']); }); test('notification routing to a specific user triggers notification to that user', function (): void { Notification::fake(); $user = User::factory()->create(); $user->assignRole('user'); $this->actingAs($user); $adminUser = User::where('email', 'admin@example.com')->firstOrFail(); $dto = new CreateTicketRequest( userId: $user->id, connectedAppId: null, issueCategory: 'Technical Issue', description: 'SSO log-in error.', notifiedRoleIds: [], notifiedUserIds: [$adminUser->id], attachment: null, ); $service = app(TicketService::class); $service->create($dto); Notification::assertSentTo( $adminUser, TicketRaisedNotification::class ); }); test('notification routing to multiple roles triggers notifications to all users assigned to those roles', function (): void { Notification::fake(); $user = User::factory()->create(); $user->assignRole('user'); $this->actingAs($user); $supportRole = Role::where('name', 'Support')->firstOrFail(); $managerRole = Role::where('name', 'Manager')->firstOrFail(); // Create a support user $supportUser = User::factory()->create(); $supportUser->assignRole($supportRole); // Create a manager user $managerUser = User::factory()->create(); $managerUser->assignRole($managerRole); $dto = new CreateTicketRequest( userId: $user->id, connectedAppId: null, issueCategory: 'Service Extension', description: 'Requesting 3 days trial extension.', notifiedRoleIds: [$supportRole->id, $managerRole->id], notifiedUserIds: [], attachment: null, ); $service = app(TicketService::class); $service->create($dto); Notification::assertSentTo( [$supportUser, $managerUser], TicketRaisedNotification::class ); }); test('admins can assign users to tickets and trigger assignment notifications', function (): void { Notification::fake(); $admin = User::where('email', 'admin@example.com')->firstOrFail(); $this->actingAs($admin); $user = User::factory()->create(); $user->assignRole('user'); $ticket = App\Models\Ticket::create([ 'user_id' => $user->id, 'issue_category' => 'Billing Query', 'status_id' => App\Models\TicketStatus::where('name', 'open')->firstOrFail()->id, ]); $supportUser = User::factory()->create(); $supportUser->assignRole('Support'); $service = app(TicketService::class); $service->assignUser($ticket->id, $supportUser->id); $this->assertDatabaseHas('tickets', [ 'id' => $ticket->id, 'assigned_user_id' => $supportUser->id, ]); Notification::assertSentTo( $supportUser, App\Notifications\TicketAssignedNotification::class ); }); test('authorized users can comment on tickets and unauthorized users are blocked', function (): void { $creator = User::factory()->create(); $creator->assignRole('user'); $unauthorizedUser = User::factory()->create(); $unauthorizedUser->assignRole('user'); $ticket = App\Models\Ticket::create([ 'user_id' => $creator->id, 'issue_category' => 'Technical Issue', 'status_id' => App\Models\TicketStatus::where('name', 'open')->firstOrFail()->id, ]); $service = app(TicketService::class); // 1. Authorized user (Creator) can comment $dto = new App\Data\Ticket\PostReplyRequest( ticketId: $ticket->id, userId: $creator->id, message: 'This is a comment from the creator.', ); $replyData = $service->postReply($dto); expect($replyData->message)->toBe('This is a comment from the creator.'); $this->assertDatabaseHas('ticket_replies', [ 'ticket_id' => $ticket->id, 'user_id' => $creator->id, 'message' => 'This is a comment from the creator.', ]); // 2. Unauthorized user gets blocked $dto2 = new App\Data\Ticket\PostReplyRequest( ticketId: $ticket->id, userId: $unauthorizedUser->id, message: 'Should be blocked.', ); expect(fn () => $service->postReply($dto2)) ->toThrow(Illuminate\Auth\Access\AuthorizationException::class); }); test('posting a comment transitions the status from open to in-progress', function (): void { $creator = User::factory()->create(); $creator->assignRole('user'); $openStatus = App\Models\TicketStatus::where('name', 'open')->firstOrFail(); $inProgressStatus = App\Models\TicketStatus::where('name', 'in-progress')->firstOrFail(); $ticket = App\Models\Ticket::create([ 'user_id' => $creator->id, 'issue_category' => 'Service Extension', 'status_id' => $openStatus->id, ]); $service = app(TicketService::class); $dto = new App\Data\Ticket\PostReplyRequest( ticketId: $ticket->id, userId: $creator->id, message: 'Transition this status.', ); $service->postReply($dto); $this->assertDatabaseHas('tickets', [ 'id' => $ticket->id, 'status_id' => $inProgressStatus->id, ]); }); test('assigned support users can see their assigned tickets on their dashboard', function (): void { $creator = User::factory()->create(); $creator->assignRole('user'); $supportUser = User::factory()->create(); $supportUser->assignRole('Support'); $openStatus = App\Models\TicketStatus::where('name', 'open')->firstOrFail(); $ticket = App\Models\Ticket::create([ 'user_id' => $creator->id, 'issue_category' => 'Technical Issue', 'status_id' => $openStatus->id, 'assigned_user_id' => $supportUser->id, ]); $service = app(TicketService::class); $tickets = $service->getListForUser($supportUser->id); expect(collect($tickets->all())->contains('id', $ticket->id))->toBeTrue(); }); test('commenting on a ticket notifies the creator and assigned user', function (): void { Notification::fake(); $creator = User::factory()->create(); $creator->assignRole('user'); $supportUser = User::factory()->create(); $supportUser->assignRole('Support'); $openStatus = App\Models\TicketStatus::where('name', 'open')->firstOrFail(); $ticket = App\Models\Ticket::create([ 'user_id' => $creator->id, 'issue_category' => 'Billing Query', 'status_id' => $openStatus->id, 'assigned_user_id' => $supportUser->id, ]); // Another admin comments $admin = User::where('email', 'admin@example.com')->firstOrFail(); $dto = new App\Data\Ticket\PostReplyRequest( ticketId: $ticket->id, userId: $admin->id, message: 'This is an admin comment.', ); $service = app(TicketService::class); $service->postReply($dto); // Both the creator and the support assignee should be notified of the comment Notification::assertSentTo( $creator, App\Notifications\TicketCommentedNotification::class ); Notification::assertSentTo( $supportUser, App\Notifications\TicketCommentedNotification::class ); // Commenter (admin) should not get notified Notification::assertNotSentTo( $admin, App\Notifications\TicketCommentedNotification::class ); }); test('raising a support ticket always triggers a notification to all administrators', function (): void { Notification::fake(); $user = User::factory()->create(); $user->assignRole('user'); $this->actingAs($user); $admin = User::where('email', 'admin@example.com')->firstOrFail(); $dto = new CreateTicketRequest( userId: $user->id, connectedAppId: null, issueCategory: 'Technical Issue', description: 'SSO log-in error.', notifiedRoleIds: [], notifiedUserIds: [], attachment: null, ); $service = app(TicketService::class); $service->create($dto); // Administrator should be notified of the new ticket Notification::assertSentTo( $admin, TicketRaisedNotification::class ); });