singleloginsystem/tests/Feature/TicketSystemTest.php
= 8a41ae6ac1 Feat: assigned user status change, search, pagination and notification
- Added activity logs for ticket status changes, comments, and ticket views.
- Implemented `TicketStatusChangedNotification` to notify users of status updates.
- Enhanced ticket list filtering with pagination, search, and status filters.
- Updated UI to better handle ticket assignments, status updates, and comment notifications.
- Extended tests for activity logs, unauthorized actions, status change notifications, and filtering functionalities.
2026-05-28 13:22:36 +00:00

389 lines
12 KiB
PHP

<?php
declare(strict_types=1);
use App\Data\Ticket\CreateTicketRequest;
use App\Enums\TicketStatusEnum;
use App\Models\{ConnectedApp, Role, User};
use App\Notifications\TicketRaisedNotification;
use App\Services\TicketService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\{Notification, Storage};
use Livewire\Livewire;
beforeEach(function (): void {
$this->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
);
});