Files
minicrm/tests/Feature/ClosingPermissionTest.php
phuongtc fc9158b928
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
Refactor: Enforce type safety, fix close deadlock, implement assignee restrictions and notify admin fallbacks
2026-05-20 10:42:30 +00:00

344 lines
13 KiB
PHP

<?php
use App\Models\Customer;
use App\Models\Department;
use App\Models\Feedback;
use App\Models\FeedbackAttachment;
use App\Models\FeedbackChannel;
use App\Models\User;
use App\Services\ClosingService;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
// Create permissions
Permission::firstOrCreate(['name' => 'close-ticket']);
Permission::firstOrCreate(['name' => 'view-feedback']);
Permission::firstOrCreate(['name' => 'create-feedback']);
Permission::firstOrCreate(['name' => 'update-feedback']);
Permission::firstOrCreate(['name' => 'add-interaction']);
Permission::firstOrCreate(['name' => 'transfer-department']);
});
// ─── Scenario 1: Staff cannot close ticket ───────────────────────────
test('staff cannot close ticket - returns 403 forbidden', function () {
$staffRole = Role::create(['name' => 'staff']);
$staffRole->givePermissionTo(['view-feedback', 'create-feedback', 'update-feedback', 'add-interaction']);
$staff = User::factory()->create();
$staff->assignRole('staff');
$channel = FeedbackChannel::create(['name' => 'Email', 'slug' => 'email']);
$customer = Customer::create(['name' => 'Test Customer', 'email' => 'test@test.com']);
$feedback = Feedback::create([
'customer_id' => $customer->id,
'feedback_channel_id' => $channel->id,
'title' => 'Test Feedback',
'content' => 'Test content',
'status' => 'resolved',
]);
$this->actingAs($staff);
$closingService = app(ClosingService::class);
try {
$closingService->close($feedback, $staff);
$this->fail('Expected HttpException (403) but none thrown.');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
expect($e->getStatusCode())->toBe(403);
expect($e->getMessage())->toBe('Chỉ lãnh đạo cấp phòng mới có quyền đóng phiếu.');
}
expect($feedback->fresh()->status)->toBe('resolved');
});
// ─── Scenario 2: Manager cannot close without proof_of_resolution ────
test('manager cannot close ticket without proof_of_resolution evidence', function () {
$managerRole = Role::create(['name' => 'manager']);
$managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'create-feedback', 'update-feedback', 'transfer-department']);
$manager = User::factory()->create();
$manager->assignRole('manager');
$dept = Department::create(['name' => 'CSKH', 'manager_id' => $manager->id]);
$channel = FeedbackChannel::create(['name' => 'Email', 'slug' => 'email-2']);
$customer = Customer::create(['name' => 'Test Customer 2', 'email' => 'test2@test.com']);
$feedback = Feedback::create([
'customer_id' => $customer->id,
'feedback_channel_id' => $channel->id,
'title' => 'Test Feedback 2',
'content' => 'Test content',
'status' => 'resolved',
'current_department_id' => $dept->id,
]);
$this->actingAs($manager);
$closingService = app(ClosingService::class);
try {
$closingService->close($feedback, $manager);
$this->fail('Expected ValidationException but none thrown.');
} catch (\Illuminate\Validation\ValidationException $e) {
expect($e->errors()['status'][0])->toBe('Yêu cầu cung cấp tài liệu bằng chứng để hoàn tất quy trình.');
}
expect($feedback->fresh()->status)->toBe('resolved');
});
// ─── Scenario 2b: Manager CAN close with proof_of_resolution ─────────
test('manager can close ticket with proof_of_resolution evidence', function () {
\Illuminate\Support\Facades\Notification::fake();
$managerRole = Role::create(['name' => 'manager']);
$managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'create-feedback', 'update-feedback', 'transfer-department']);
$manager = User::factory()->create();
$manager->assignRole('manager');
$dept = Department::create(['name' => 'Ky Thuat', 'manager_id' => $manager->id]);
$channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone']);
$customer = Customer::create(['name' => 'Test Customer 3', 'email' => 'test3@test.com']);
$feedback = Feedback::create([
'customer_id' => $customer->id,
'feedback_channel_id' => $channel->id,
'title' => 'Test Feedback 3',
'content' => 'Test content',
'status' => 'resolved',
'current_department_id' => $dept->id,
]);
FeedbackAttachment::create([
'uuid' => 'test-uuid-123',
'feedback_id' => $feedback->id,
'user_id' => $manager->id,
'name' => 'proof.pdf',
'path' => 'feedback-attachments/proof.pdf',
'disk' => 'local',
'collection' => 'proof_of_resolution',
'mime_type' => 'application/pdf',
'size' => 1024,
]);
$this->actingAs($manager);
$closingService = app(ClosingService::class);
$closingService->close($feedback, $manager);
expect($feedback->fresh()->status)->toBe('closed');
});
// ─── Scenario 2c: Manager cannot close ticket from other department ──
test('manager cannot close ticket from department they do not manage', function () {
$managerRole = Role::create(['name' => 'manager']);
$managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'create-feedback', 'update-feedback', 'transfer-department']);
$manager = User::factory()->create();
$manager->assignRole('manager');
$deptA = Department::create(['name' => 'CSKH', 'manager_id' => $manager->id]);
$deptB = Department::create(['name' => 'Ky Thuat', 'manager_id' => null]);
$channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-2']);
$customer = Customer::create(['name' => 'Test Customer 4', 'email' => 'test4@test.com']);
$feedback = Feedback::create([
'customer_id' => $customer->id,
'feedback_channel_id' => $channel->id,
'title' => 'Test Feedback 4',
'content' => 'Test content',
'status' => 'resolved',
'current_department_id' => $deptB->id,
]);
FeedbackAttachment::create([
'uuid' => 'test-uuid-456',
'feedback_id' => $feedback->id,
'user_id' => $manager->id,
'name' => 'proof.pdf',
'path' => 'feedback-attachments/proof2.pdf',
'disk' => 'local',
'collection' => 'proof_of_resolution',
'mime_type' => 'application/pdf',
'size' => 1024,
]);
$this->actingAs($manager);
$closingService = app(ClosingService::class);
try {
$closingService->close($feedback, $manager);
$this->fail('Expected HttpException (403) but none thrown.');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
expect($e->getStatusCode())->toBe(403);
expect($e->getMessage())->toBe('Bạn chỉ có thể đóng phiếu thuộc phòng ban mình quản lý.');
}
expect($feedback->fresh()->status)->toBe('resolved');
});
// ─── Scenario 2d: Staff with close permission cannot close ticket from other department ──
test('staff with close permission cannot close ticket from other department', function () {
$staffRole = Role::create(['name' => 'staff']);
$staffRole->givePermissionTo(['close-ticket', 'view-feedback', 'update-feedback']);
// Staff belongs to Department A
$deptA = Department::create(['name' => 'CSKH']);
$staff = User::factory()->create(['department_id' => $deptA->id]);
$staff->assignRole('staff');
// Ticket belongs to Department B
$deptB = Department::create(['name' => 'Ky Thuat']);
$channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-staff-close']);
$customer = Customer::create(['name' => 'Test Customer Staff', 'email' => 'staff@test.com']);
$feedback = Feedback::create([
'customer_id' => $customer->id,
'feedback_channel_id' => $channel->id,
'title' => 'Test Feedback Staff',
'content' => 'Test content',
'status' => 'resolved',
'current_department_id' => $deptB->id,
]);
FeedbackAttachment::create([
'uuid' => 'test-uuid-staff-1',
'feedback_id' => $feedback->id,
'user_id' => $staff->id,
'name' => 'proof.pdf',
'path' => 'feedback-attachments/proof_staff.pdf',
'disk' => 'local',
'collection' => 'proof_of_resolution',
'mime_type' => 'application/pdf',
'size' => 1024,
]);
$this->actingAs($staff);
$closingService = app(ClosingService::class);
try {
$closingService->close($feedback, $staff);
$this->fail('Expected HttpException (403) but none thrown.');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
expect($e->getStatusCode())->toBe(403);
expect($e->getMessage())->toBe('Bạn chỉ có thể đóng phiếu thuộc phòng ban mình quản lý.');
}
expect($feedback->fresh()->status)->toBe('resolved');
});
// ─── Scenario 2e: Staff with close permission can close ticket from same department ────
test('staff with close permission can close ticket from same department', function () {
$staffRole = Role::create(['name' => 'staff']);
$staffRole->givePermissionTo(['close-ticket', 'view-feedback', 'update-feedback']);
// Staff belongs to Department A
$deptA = Department::create(['name' => 'CSKH']);
$staff = User::factory()->create(['department_id' => $deptA->id]);
$staff->assignRole('staff');
$channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-staff-close-success']);
$customer = Customer::create(['name' => 'Test Customer Staff Success', 'email' => 'staff_success@test.com']);
$feedback = Feedback::create([
'customer_id' => $customer->id,
'feedback_channel_id' => $channel->id,
'title' => 'Test Feedback Staff Success',
'content' => 'Test content',
'status' => 'resolved',
'current_department_id' => $deptA->id,
]);
FeedbackAttachment::create([
'uuid' => 'test-uuid-staff-2',
'feedback_id' => $feedback->id,
'user_id' => $staff->id,
'name' => 'proof.pdf',
'path' => 'feedback-attachments/proof_staff2.pdf',
'disk' => 'local',
'collection' => 'proof_of_resolution',
'mime_type' => 'application/pdf',
'size' => 1024,
]);
$this->actingAs($staff);
$closingService = app(ClosingService::class);
$closingService->close($feedback, $staff);
expect($feedback->fresh()->status)->toBe('closed');
});
// ─── Scenario 2f: Notify admin when closing a department ticket with no manager ────────
test('admin notification is sent when closing department ticket with no manager', function () {
\Illuminate\Support\Facades\Notification::fake();
// Create admin role and user
$adminRole = Role::create(['name' => 'admin']);
$admin = User::factory()->create();
$admin->assignRole('admin');
$managerRole = Role::create(['name' => 'manager']);
$managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'update-feedback']);
// Department with no manager
$dept = Department::create(['name' => 'Unmanaged Dept', 'manager_id' => null]);
$manager = User::factory()->create(['department_id' => $dept->id]);
$manager->assignRole('manager');
$channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-no-manager']);
$customer = Customer::create(['name' => 'Customer', 'email' => 'no_manager@test.com']);
$feedback = Feedback::create([
'customer_id' => $customer->id,
'feedback_channel_id' => $channel->id,
'title' => 'Ticket',
'content' => 'Content',
'status' => 'resolved',
'current_department_id' => $dept->id,
]);
FeedbackAttachment::create([
'uuid' => 'test-uuid-no-manager',
'feedback_id' => $feedback->id,
'user_id' => $manager->id,
'name' => 'proof.pdf',
'path' => 'feedback-attachments/proof3.pdf',
'disk' => 'local',
'collection' => 'proof_of_resolution',
'mime_type' => 'application/pdf',
'size' => 1024,
]);
$this->actingAs($manager);
$closingService = app(ClosingService::class);
$closingService->close($feedback, $manager);
expect($feedback->fresh()->status)->toBe('closed');
// Assert notification sent to admin
\Illuminate\Support\Facades\Notification::assertSentTo(
$admin,
\App\Notifications\TicketClosed::class,
function ($notification) use ($feedback, $manager) {
return $notification->feedback->id === $feedback->id
&& $notification->actor->id === $manager->id;
}
);
});