- add rotating admin refresh tokens and password gates\n- expand ticket galleries, exports, and client resources\n- improve IP allocation auditing and dashboard data\n- enforce encrypted client access tokens on public routes
171 lines
7.2 KiB
PHP
171 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Api\Client;
|
|
|
|
use App\Models\Customer;
|
|
use App\Models\Ticket;
|
|
use App\Mail\Admin\TicketAdminNotification;
|
|
use App\Mail\TicketCreated;
|
|
use App\Models\TicketReply;
|
|
use App\Models\User;
|
|
use App\Support\TicketGallery;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
class ClientTicketTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private Customer $customer;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->customer = Customer::factory()->create(['status' => 1]);
|
|
}
|
|
|
|
private function makeTicket(Customer $owner, array $attributes = []): Ticket
|
|
{
|
|
return Ticket::create(array_merge([
|
|
'uuid' => (string) Str::uuid(),
|
|
'ticket_num' => 'TKT-' . Str::upper(Str::random(6)),
|
|
'subject' => 'Subject',
|
|
'description' => 'Description',
|
|
'status' => 'open',
|
|
'room' => 'technical',
|
|
'customer_id' => $owner->id,
|
|
], $attributes));
|
|
}
|
|
|
|
private function as(Customer $customer): static
|
|
{
|
|
return $this->actingAs($customer, 'client')->withHeaders(['x-access-token' => $this->buildClientAccessToken()]);
|
|
}
|
|
|
|
private function list(array $query = [])
|
|
{
|
|
return $this->as($this->customer)->getJson('/v1/client/tickets?' . http_build_query($query));
|
|
}
|
|
|
|
public function test_paginates_with_requested_page_size(): void
|
|
{
|
|
foreach (range(1, 12) as $i) {
|
|
$this->makeTicket($this->customer, ['subject' => "Ticket {$i}"]);
|
|
}
|
|
$this->makeTicket(Customer::factory()->create(['status' => 1]));
|
|
|
|
$response = $this->list(['per_page' => 5, 'page' => 3])->assertOk();
|
|
|
|
$this->assertSame(12, $response->json('data.total'));
|
|
$this->assertSame(3, $response->json('data.last_page'));
|
|
$this->assertCount(2, $response->json('data.items'));
|
|
}
|
|
|
|
public function test_filters_by_search_room_and_status(): void
|
|
{
|
|
$this->makeTicket($this->customer, ['subject' => 'Lỗi VPS không khởi động', 'ticket_num' => 'TKT-VPS']);
|
|
$this->makeTicket($this->customer, ['subject' => 'Báo giá hosting', 'room' => 'sales']);
|
|
$this->makeTicket($this->customer, ['subject' => 'VPS chậm', 'status' => 'closed']);
|
|
$this->makeTicket($this->customer, ['subject' => '100% CPU']);
|
|
|
|
$this->assertSame(['TKT-VPS'], collect($this->list(['search' => 'khởi động'])->json('data.items'))->pluck('ticket_num')->all());
|
|
$this->assertCount(1, $this->list(['search' => 'tkt-vps'])->json('data.items'));
|
|
$this->assertCount(1, $this->list(['search' => '100%'])->json('data.items'));
|
|
$this->assertCount(1, $this->list(['room' => 'sales'])->json('data.items'));
|
|
$this->assertCount(1, $this->list(['search' => 'VPS', 'status' => 'open'])->json('data.items'));
|
|
}
|
|
|
|
public function test_rejects_oversized_page(): void
|
|
{
|
|
$this->list(['per_page' => 500])->assertStatus(422);
|
|
}
|
|
|
|
public function test_client_payload_hides_internal_ids_and_storage_urls(): void
|
|
{
|
|
Storage::fake('public');
|
|
Storage::disk('public')->put('tickets/legacy.webp', 'legacy');
|
|
$ticket = $this->makeTicket($this->customer, [
|
|
'ticket_num' => 'TKT-LEGACY',
|
|
'data' => json_encode(['priority' => 'high']),
|
|
'gallery' => ['https://api.portal.test/storage/tickets/legacy.webp'],
|
|
]);
|
|
TicketReply::create(['ticket_id' => $ticket->id, 'message' => 'Hi', 'user_id' => User::factory()->create()->id]);
|
|
|
|
$data = $this->as($this->customer)->getJson('/v1/client/tickets/TKT-LEGACY')->assertOk()->json('data');
|
|
|
|
foreach (['id', 'uuid', 'customer_id', 'data'] as $key) {
|
|
$this->assertArrayNotHasKey($key, $data);
|
|
}
|
|
$this->assertSame(['fullname'], array_keys($data['customer']));
|
|
$this->assertSame('high', $data['priority']);
|
|
$this->assertSame(['/v1/client/tickets/TKT-LEGACY/images/0'], $data['gallery']);
|
|
$this->assertSame(['message', 'sender_type', 'sender_name', 'sender_role', 'created_at'], array_keys($data['replies'][0]));
|
|
$this->assertStringNotContainsString('storage', json_encode($data));
|
|
|
|
$this->as($this->customer)->get('/v1/client/tickets/TKT-LEGACY/images/0')->assertOk();
|
|
$this->as($this->customer)->get('/v1/client/tickets/TKT-LEGACY/images/1')->assertNotFound();
|
|
}
|
|
|
|
public function test_uploaded_images_are_private_and_owner_only(): void
|
|
{
|
|
Storage::fake('local');
|
|
Mail::fake();
|
|
Queue::fake();
|
|
|
|
$ticketNum = $this->as($this->customer)->post('/v1/client/tickets', [
|
|
'subject' => 'Cần hỗ trợ VPS',
|
|
'description' => 'Mô tả chi tiết vấn đề đang gặp phải',
|
|
'images' => [UploadedFile::fake()->image('shot.png')],
|
|
])->assertCreated()->json('data.ticket_num');
|
|
|
|
$path = Ticket::where('ticket_num', $ticketNum)->value('gallery')[0];
|
|
$this->assertStringStartsWith("ticket-attachments/{$this->customer->id}/", $path);
|
|
Storage::disk('local')->assertExists($path);
|
|
|
|
$this->as($this->customer)->get("/v1/client/tickets/{$ticketNum}/images/0")->assertOk();
|
|
$this->as(Customer::factory()->create(['status' => 1]))->get("/v1/client/tickets/{$ticketNum}/images/0")->assertNotFound();
|
|
|
|
$this->get("/api/v1/administrator/tickets/{$ticketNum}/images/0")->assertForbidden();
|
|
$signed = URL::temporarySignedRoute('admin.tickets.image', now()->addMinute(), ['ticket' => $ticketNum, 'index' => 0]);
|
|
$this->get($signed)->assertOk();
|
|
}
|
|
|
|
public function test_moves_legacy_public_images_to_private_disk(): void
|
|
{
|
|
Storage::fake('public');
|
|
Storage::fake('local');
|
|
Storage::disk('public')->put('tickets/old.webp', 'old');
|
|
$ticket = $this->makeTicket($this->customer, ['gallery' => ['https://api.portal.test/storage/tickets/old.webp']]);
|
|
|
|
$this->assertTrue(TicketGallery::moveLegacyToPrivate($ticket));
|
|
|
|
$path = "ticket-attachments/{$this->customer->id}/old.webp";
|
|
$this->assertSame([$path], $ticket->fresh()->gallery);
|
|
Storage::disk('local')->assertExists($path);
|
|
Storage::disk('public')->assertMissing('tickets/old.webp');
|
|
$this->assertFalse(TicketGallery::moveLegacyToPrivate($ticket->fresh()));
|
|
}
|
|
|
|
public function test_ticket_emails_show_customer_fullname_and_room_label(): void
|
|
{
|
|
$this->seed(\Database\Seeders\TemplateSeeder::class);
|
|
$this->customer->update(['fullname' => 'Nguyễn Văn A']);
|
|
$ticket = $this->makeTicket($this->customer, ['ticket_num' => 'TKT-MAIL']);
|
|
|
|
foreach ([new TicketCreated($ticket, $this->customer), new TicketAdminNotification($ticket, $this->customer)] as $mail) {
|
|
$html = $mail->render();
|
|
$this->assertStringContainsString('Nguyễn Văn A', $html, $mail::class);
|
|
$this->assertStringContainsString('Kỹ thuật', $html, $mail::class);
|
|
$this->assertStringNotContainsString('N/A', $html, $mail::class);
|
|
}
|
|
}
|
|
}
|