new helpdesk module
This commit is contained in:
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\tests\Browser;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Helpdesk\Models\Priority;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
final class HelpdeskBrowserTest extends DuskTestCase
|
||||
{
|
||||
use DatabaseMigrations;
|
||||
|
||||
/** @test */
|
||||
public function a_user_can_open_the_helpdesk_index_and_create_page(): void
|
||||
{
|
||||
Status::query()->create(['name' => 'Open', 'slug' => 'open', 'color' => 'blue']);
|
||||
Priority::query()->create(['name' => 'Normal', 'slug' => 'normal', 'color' => 'blue']);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser->loginAs($user)
|
||||
->visitRoute('helpdesk.tickets.index')
|
||||
->waitFor('@helpdesk-ticket-index')
|
||||
->assertSee('Helpdesk')
|
||||
->clickLink('New ticket')
|
||||
->waitFor('@helpdesk-ticket-create')
|
||||
->assertSee('Create ticket');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Browser;
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
final class HelpdeskKanbanBrowserTest extends DuskTestCase
|
||||
{
|
||||
use BuildsHelpdeskFixtures;
|
||||
use DatabaseMigrations;
|
||||
|
||||
/** @test */
|
||||
public function kanban_board_is_available_from_helpdesk_module(): void
|
||||
{
|
||||
$lookups = $this->seedLookupData();
|
||||
$user = $this->admin();
|
||||
|
||||
$this->ticket([
|
||||
'owner' => $user,
|
||||
'status_id' => $lookups['open']->id,
|
||||
'priority_id' => $lookups['high']->id,
|
||||
'subject' => 'Kanban browser ticket',
|
||||
]);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser->loginAs($user)
|
||||
->visitRoute('helpdesk.kanban')
|
||||
->assertPresent('@helpdesk-kanban-board')
|
||||
->assertSee('Kanban board')
|
||||
->assertSee('Kanban browser ticket')
|
||||
->assertPresent('@kanban-column-open');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Browser;
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
final class HelpdeskNavigationBrowserTest extends DuskTestCase
|
||||
{
|
||||
use DatabaseMigrations;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_helpdesk_dashboard_and_main_pages_render_with_professional_headers(): void
|
||||
{
|
||||
$this->seedLookupData();
|
||||
$user = $this->admin(['name' => 'Mark Gregory']);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser->loginAs($user)
|
||||
->visitRoute('helpdesk.index')
|
||||
->waitFor('@helpdesk-dashboard-widget')
|
||||
->assertSee('Helpdesk dashboard')
|
||||
->assertSee('Total tickets')
|
||||
->assertSee('Open tickets')
|
||||
->assertSee('Closed tickets')
|
||||
->clickLink('Tickets')
|
||||
->waitFor('@helpdesk-ticket-index')
|
||||
->assertSee('Active tickets')
|
||||
->clickLink('New ticket')
|
||||
->waitFor('@helpdesk-ticket-create')
|
||||
->assertSee('Create ticket')
|
||||
->visitRoute('helpdesk.settings')
|
||||
->waitFor('@helpdesk-settings-manager')
|
||||
->assertSee('Support configuration')
|
||||
->assertSee('Statuses')
|
||||
->assertSee('Priorities')
|
||||
->assertSee('Projects')
|
||||
->assertSee('Workflow');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Browser;
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
final class HelpdeskSettingsBrowserTest extends DuskTestCase
|
||||
{
|
||||
use DatabaseMigrations;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_settings_tabs_expose_lookup_git_and_workflow_setup_areas(): void
|
||||
{
|
||||
$this->seedLookupData();
|
||||
$admin = $this->admin(['name' => 'Settings Admin']);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($admin): void {
|
||||
$browser->loginAs($admin)
|
||||
->visitRoute('helpdesk.settings')
|
||||
->waitFor('@helpdesk-settings-manager')
|
||||
->assertSee('Support configuration')
|
||||
->assertSee('Statuses')
|
||||
->assertSee('Priorities')
|
||||
->assertSee('Categories')
|
||||
->assertSee('Teams')
|
||||
->assertSee('Projects')
|
||||
->assertSee('Git')
|
||||
->assertSee('Workflow')
|
||||
->press('Git')
|
||||
->waitForText('Git repositories')
|
||||
->assertSee('Remote URL')
|
||||
->press('Workflow')
|
||||
->waitForText('Workflow messages')
|
||||
->assertSee('HTML email body');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Browser;
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
final class HelpdeskTicketBrowserTest extends DuskTestCase
|
||||
{
|
||||
use DatabaseMigrations;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_user_can_create_and_view_ticket_in_browser(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$user = $this->user(['name' => 'Ticket Browser User']);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user, $fixtures): void {
|
||||
$browser->loginAs($user)
|
||||
->visitRoute('helpdesk.tickets.create')
|
||||
->waitFor('@helpdesk-ticket-create')
|
||||
->type('@ticket-subject', 'Browser created helpdesk ticket')
|
||||
->script("document.querySelector('[dusk=\"ticket-body-editor\"] [contenteditable=\"true\"]').innerHTML = '<p>This browser ticket proves the form saves correctly.</p>'; document.querySelector('[dusk=\"ticket-body-editor\"] [contenteditable=\"true\"]').dispatchEvent(new Event('input', { bubbles: true }));");
|
||||
|
||||
$browser->select('@ticket-priority', (string) $fixtures['high']->id)
|
||||
->select('@ticket-category', (string) $fixtures['category']->id)
|
||||
->select('@ticket-team', (string) $fixtures['team']->id)
|
||||
->select('@ticket-project', (string) $fixtures['project']->id)
|
||||
->press('@save-ticket')
|
||||
->waitFor('@helpdesk-ticket-show')
|
||||
->assertSee('Browser created helpdesk ticket')
|
||||
->assertSee('High')
|
||||
->assertSee('Support')
|
||||
->assertSee('Ticket details');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_user_can_add_nested_response_in_browser(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$user = $this->user(['name' => 'Response Browser User']);
|
||||
$ticket = $this->ticket(['owner' => $user, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id, 'subject' => 'Browser response ticket']);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user, $ticket): void {
|
||||
$browser->loginAs($user)
|
||||
->visitRoute('helpdesk.tickets.show', $ticket)
|
||||
->waitFor('@helpdesk-ticket-show')
|
||||
->script("document.querySelector('[dusk=\"ticket-comment-editor\"] [contenteditable=\"true\"]').innerHTML = '<p>First browser response</p>'; document.querySelector('[dusk=\"ticket-comment-editor\"] [contenteditable=\"true\"]').dispatchEvent(new Event('input', { bubbles: true }));");
|
||||
|
||||
$browser->press('@post-comment')
|
||||
->waitForText('First browser response')
|
||||
->assertSee('First browser response');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Concerns;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Helpdesk\Models\Category;
|
||||
use Modules\Helpdesk\Models\GitRepository;
|
||||
use Modules\Helpdesk\Models\Priority;
|
||||
use Modules\Helpdesk\Models\Project;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Team;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
|
||||
trait BuildsHelpdeskFixtures
|
||||
{
|
||||
protected function user(array $attributes = []): User
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = User::factory()->create(array_merge([
|
||||
'name' => 'Helpdesk User',
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'password' => Hash::make('password'),
|
||||
], $attributes));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function agent(array $attributes = []): User
|
||||
{
|
||||
$user = $this->user($attributes);
|
||||
$user->forceFill(['helpdesk_agent' => true])->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function admin(array $attributes = []): User
|
||||
{
|
||||
$user = $this->user($attributes);
|
||||
$user->forceFill(['helpdesk_agent' => true, 'helpdesk_admin' => true])->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function status(array $attributes = []): Status
|
||||
{
|
||||
return Status::query()->create(array_merge([
|
||||
'name' => 'Open',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'color' => 'blue',
|
||||
'sort_order' => 10,
|
||||
'is_closed' => false,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function closedStatus(array $attributes = []): Status
|
||||
{
|
||||
return $this->status(array_merge([
|
||||
'name' => 'Closed',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'color' => 'emerald',
|
||||
'sort_order' => 99,
|
||||
'is_closed' => true,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function priority(array $attributes = []): Priority
|
||||
{
|
||||
return Priority::query()->create(array_merge([
|
||||
'name' => 'Normal',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'color' => 'blue',
|
||||
'sort_order' => 10,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function category(array $attributes = []): Category
|
||||
{
|
||||
return Category::query()->create(array_merge([
|
||||
'company_id' => null,
|
||||
'name' => 'Support',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'color' => 'blue',
|
||||
'description' => 'Support requests',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function team(array $attributes = []): Team
|
||||
{
|
||||
return Team::query()->create(array_merge([
|
||||
'company_id' => null,
|
||||
'name' => 'Support Team',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'color' => 'indigo',
|
||||
'description' => 'Primary support team',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function project(array $attributes = []): Project
|
||||
{
|
||||
return Project::query()->create(array_merge([
|
||||
'company_id' => null,
|
||||
'team_id' => null,
|
||||
'title' => 'WMS Project',
|
||||
'slug' => fake()->unique()->slug(2),
|
||||
'color' => 'blue',
|
||||
'description' => 'Warehouse platform project',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function repository(array $attributes = []): GitRepository
|
||||
{
|
||||
return GitRepository::query()->create(array_merge([
|
||||
'company_id' => null,
|
||||
'project_id' => null,
|
||||
'name' => 'Core',
|
||||
'provider' => 'git',
|
||||
'remote_url' => 'git@example.com:wms/core.git',
|
||||
'default_branch' => 'develop',
|
||||
'webhook_secret' => 'test-secret',
|
||||
'is_active' => true,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function ticket(array $attributes = []): Ticket
|
||||
{
|
||||
$owner = $attributes['owner'] ?? $this->user(['name' => 'Requester']);
|
||||
$agent = $attributes['agent'] ?? null;
|
||||
unset($attributes['owner'], $attributes['agent']);
|
||||
|
||||
return Ticket::query()->create(array_merge([
|
||||
'reference' => 'HD-' . now()->format('Y') . '-' . str_pad((string) random_int(1, 99999), 5, '0', STR_PAD_LEFT),
|
||||
'user_id' => $owner->id,
|
||||
'agent_id' => $agent?->id,
|
||||
'status_id' => $this->status()->id,
|
||||
'priority_id' => $this->priority()->id,
|
||||
'category_id' => null,
|
||||
'project_id' => null,
|
||||
'team_id' => null,
|
||||
'subject' => 'Test ticket subject',
|
||||
'body' => '<p>Detailed helpdesk ticket body.</p>',
|
||||
'last_activity_at' => now(),
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
protected function seedLookupData(): array
|
||||
{
|
||||
$open = $this->status(['name' => 'Open', 'slug' => 'open']);
|
||||
$closed = $this->closedStatus(['name' => 'Closed', 'slug' => 'closed']);
|
||||
$normal = $this->priority(['name' => 'Normal', 'slug' => 'normal']);
|
||||
$high = $this->priority(['name' => 'High', 'slug' => 'high', 'color' => 'red', 'sort_order' => 1]);
|
||||
$category = $this->category(['name' => 'Support', 'slug' => 'support']);
|
||||
$team = $this->team(['name' => 'Support Team', 'slug' => 'support-team']);
|
||||
$project = $this->project(['title' => 'WMS Core', 'slug' => 'wms-core', 'team_id' => $team->id]);
|
||||
|
||||
return compact('open', 'closed', 'normal', 'high', 'category', 'team', 'project');
|
||||
}
|
||||
|
||||
protected function ensureCompanyTableHasOne(): ?int
|
||||
{
|
||||
if (! Schema::hasTable('companies')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$columns = Schema::getColumnListing('companies');
|
||||
$payload = [];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
if (in_array($column, ['id', 'created_at', 'updated_at', 'deleted_at'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($column === 'name') {
|
||||
$payload[$column] = 'MPBA Test Company';
|
||||
}
|
||||
}
|
||||
|
||||
if (! array_key_exists('name', $payload) && in_array('company_name', $columns, true)) {
|
||||
$payload['company_name'] = 'MPBA Test Company';
|
||||
}
|
||||
|
||||
return (int) DB::table('companies')->insertGetId($payload);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Helpdesk\Services\AgentAssignmentService;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class AgentAssignmentTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_assignment_selects_least_loaded_agent(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$busy = $this->agent(['name' => 'Busy Agent']);
|
||||
$quiet = $this->agent(['name' => 'Quiet Agent']);
|
||||
$this->ticket(['agent' => $busy, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
$this->ticket(['agent' => $busy, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
|
||||
$selected = app(AgentAssignmentService::class)->selectAgentId(null);
|
||||
|
||||
self::assertSame($quiet->id, $selected);
|
||||
}
|
||||
|
||||
public function test_assignment_prefers_category_agents_when_category_has_agents(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$generalAgent = $this->agent(['name' => 'General Agent']);
|
||||
$categoryAgent = $this->agent(['name' => 'Category Agent']);
|
||||
$fixtures['category']->agents()->attach($categoryAgent->id);
|
||||
$this->ticket(['agent' => $generalAgent, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
|
||||
$selected = app(AgentAssignmentService::class)->selectAgentId($fixtures['category']->id);
|
||||
|
||||
self::assertSame($categoryAgent->id, $selected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Helpdesk\Models\Attachment;
|
||||
use Modules\Helpdesk\Models\GitCommit;
|
||||
use Modules\Helpdesk\Services\AttachmentService;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class GitAndAttachmentTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_git_commit_can_be_tracked_against_repository_and_ticket(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$repository = $this->repository(['project_id' => $fixtures['project']->id]);
|
||||
$ticket = $this->ticket(['project_id' => $fixtures['project']->id]);
|
||||
|
||||
GitCommit::query()->create([
|
||||
'repository_id' => $repository->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'hash' => str_repeat('b', 40),
|
||||
'short_hash' => 'bbbbbbb',
|
||||
'branch' => 'develop',
|
||||
'author_name' => 'Mark Gregory',
|
||||
'author_email' => 'mark@example.com',
|
||||
'message' => 'Resolve ' . $ticket->reference . ' ticket save redirect',
|
||||
'url' => 'https://example.com/commit/b',
|
||||
'committed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_commits', [
|
||||
'repository_id' => $repository->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'short_hash' => 'bbbbbbb',
|
||||
]);
|
||||
|
||||
self::assertSame($ticket->id, $repository->commits()->firstOrFail()->ticket_id);
|
||||
}
|
||||
|
||||
public function test_attachment_service_stores_and_deletes_ticket_files(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
config()->set('helpdesk.disk', 'local');
|
||||
|
||||
$user = $this->user();
|
||||
$ticket = $this->ticket(['owner' => $user]);
|
||||
$file = UploadedFile::fake()->create('evidence.pdf', 32, 'application/pdf');
|
||||
|
||||
app(AttachmentService::class)->storeMany($ticket, [$file], $user->id);
|
||||
|
||||
$attachment = Attachment::query()->where('ticket_id', $ticket->id)->firstOrFail();
|
||||
Storage::disk('local')->assertExists($attachment->path);
|
||||
self::assertSame('evidence.pdf', $attachment->original_name);
|
||||
|
||||
app(AttachmentService::class)->delete($attachment);
|
||||
|
||||
Storage::disk('local')->assertMissing($attachment->path);
|
||||
$this->assertDatabaseMissing('helpdesk_attachments', ['id' => $attachment->id]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class HelpdeskRouteTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_authenticated_user_can_open_dashboard_tickets_create_and_settings_pages(): void
|
||||
{
|
||||
$this->seedLookupData();
|
||||
$user = $this->admin();
|
||||
|
||||
$this->actingAs($user)->get(route('helpdesk.index'))->assertOk()->assertSee('Helpdesk', false);
|
||||
$this->actingAs($user)->get(route('helpdesk.tickets.index'))->assertOk()->assertSee('Active tickets', false);
|
||||
$this->actingAs($user)->get(route('helpdesk.tickets.create'))->assertOk()->assertSee('Create ticket', false);
|
||||
$this->actingAs($user)->get(route('helpdesk.settings'))->assertOk()->assertSee('Support configuration', false);
|
||||
}
|
||||
|
||||
public function test_guest_is_redirected_from_helpdesk_pages_when_application_uses_auth_middleware(): void
|
||||
{
|
||||
$response = $this->get(route('helpdesk.tickets.index'));
|
||||
|
||||
self::assertTrue(in_array($response->getStatusCode(), [200, 302], true));
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\TicketCreate;
|
||||
use Modules\Helpdesk\Models\Priority;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class HelpdeskTicketTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @test */
|
||||
public function a_user_can_create_a_ticket(): void
|
||||
{
|
||||
Status::query()->create(['name' => 'Open', 'slug' => 'open', 'color' => 'blue']);
|
||||
Priority::query()->create(['name' => 'Normal', 'slug' => 'normal', 'color' => 'blue']);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(TicketCreate::class)
|
||||
->set('subject', 'Printer is not working')
|
||||
->set('body', 'The office printer is showing an error code and will not print.')
|
||||
->set('priority_id', Priority::query()->firstOrFail()->id)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_tickets', [
|
||||
'subject' => 'Printer is not working',
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\KanbanBoard;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class KanbanBoardTest extends TestCase
|
||||
{
|
||||
use BuildsHelpdeskFixtures;
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @test */
|
||||
public function kanban_board_renders_visible_tickets_by_status(): void
|
||||
{
|
||||
$lookups = $this->seedLookupData();
|
||||
$user = $this->admin();
|
||||
|
||||
$ticket = $this->ticket([
|
||||
'owner' => $user,
|
||||
'status_id' => $lookups['open']->id,
|
||||
'priority_id' => $lookups['high']->id,
|
||||
'subject' => 'Kanban visible ticket',
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(KanbanBoard::class)
|
||||
->assertSee('Kanban board')
|
||||
->assertSee($ticket->reference)
|
||||
->assertSee('Kanban visible ticket')
|
||||
->assertSee('High');
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function kanban_board_can_move_ticket_to_closed_status(): void
|
||||
{
|
||||
$lookups = $this->seedLookupData();
|
||||
$user = $this->admin();
|
||||
|
||||
$ticket = $this->ticket([
|
||||
'owner' => $user,
|
||||
'status_id' => $lookups['open']->id,
|
||||
'priority_id' => $lookups['normal']->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(KanbanBoard::class)
|
||||
->call('moveTicket', $ticket->id, $lookups['closed']->id)
|
||||
->assertHasNoErrors();
|
||||
|
||||
$ticket->refresh();
|
||||
|
||||
$this->assertSame($lookups['closed']->id, $ticket->status_id);
|
||||
$this->assertNotNull($ticket->completed_at);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\SettingsManager;
|
||||
use Modules\Helpdesk\Models\Category;
|
||||
use Modules\Helpdesk\Models\GitRepository;
|
||||
use Modules\Helpdesk\Models\Priority;
|
||||
use Modules\Helpdesk\Models\Project;
|
||||
use Modules\Helpdesk\Models\Status;
|
||||
use Modules\Helpdesk\Models\Team;
|
||||
use Modules\Helpdesk\Models\WorkflowTemplate;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class SettingsCrudTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_settings_manager_creates_updates_and_deletes_statuses(): void
|
||||
{
|
||||
$admin = $this->admin();
|
||||
|
||||
$component = Livewire::actingAs($admin)
|
||||
->test(SettingsManager::class)
|
||||
->set('statusForm.name', 'Awaiting Customer')
|
||||
->set('statusForm.color', 'amber')
|
||||
->set('statusForm.sort_order', 20)
|
||||
->set('statusForm.is_closed', false)
|
||||
->call('saveStatus')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$status = Status::query()->where('slug', 'awaiting-customer')->firstOrFail();
|
||||
|
||||
$component->call('editStatus', $status->id)
|
||||
->set('statusForm.name', 'Waiting Customer')
|
||||
->call('saveStatus')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_statuses', ['id' => $status->id, 'name' => 'Waiting Customer']);
|
||||
|
||||
$component->call('deleteStatus', $status->id);
|
||||
$this->assertDatabaseMissing('helpdesk_statuses', ['id' => $status->id]);
|
||||
}
|
||||
|
||||
public function test_settings_manager_creates_lookup_project_team_repository_and_workflow_records(): void
|
||||
{
|
||||
$admin = $this->admin();
|
||||
|
||||
Livewire::actingAs($admin)
|
||||
->test(SettingsManager::class)
|
||||
->set('priorityForm.name', 'Critical')
|
||||
->set('priorityForm.color', 'red')
|
||||
->set('priorityForm.sort_order', 1)
|
||||
->call('savePriority')
|
||||
->set('categoryForm.name', 'Bug Fix')
|
||||
->set('categoryForm.color', 'rose')
|
||||
->set('categoryForm.description', 'Production defects')
|
||||
->call('saveCategory')
|
||||
->set('teamForm.name', 'Platform Support')
|
||||
->set('teamForm.color', 'indigo')
|
||||
->set('teamForm.description', 'Platform team')
|
||||
->call('saveTeam')
|
||||
->set('projectForm.title', 'Core WMS')
|
||||
->set('projectForm.team_id', Team::query()->where('slug', 'platform-support')->firstOrFail()->id)
|
||||
->set('projectForm.color', 'blue')
|
||||
->set('projectForm.description', 'Core platform')
|
||||
->call('saveProject')
|
||||
->set('repositoryForm.name', 'WMS Core')
|
||||
->set('repositoryForm.project_id', Project::query()->where('slug', 'core-wms')->firstOrFail()->id)
|
||||
->set('repositoryForm.provider', 'github')
|
||||
->set('repositoryForm.remote_url', 'git@github.com:mpba/wms-core.git')
|
||||
->set('repositoryForm.default_branch', 'develop')
|
||||
->set('repositoryForm.webhook_secret', 'secret')
|
||||
->call('saveRepository')
|
||||
->set('workflowForm.name', 'Ticket Raised')
|
||||
->set('workflowForm.event', 'ticket.created')
|
||||
->set('workflowForm.subject', 'Ticket {{ ticket.reference }} created')
|
||||
->set('workflowForm.html_body', '<p>Hello {{ requester.name }}</p>')
|
||||
->set('workflowForm.text_body', 'Hello {{ requester.name }}')
|
||||
->set('workflowForm.to_role', 'requester')
|
||||
->call('saveWorkflowTemplate')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_priorities', ['slug' => 'critical']);
|
||||
$this->assertDatabaseHas('helpdesk_categories', ['slug' => 'bug-fix']);
|
||||
$this->assertDatabaseHas('helpdesk_teams', ['slug' => 'platform-support']);
|
||||
$this->assertDatabaseHas('helpdesk_projects', ['slug' => 'core-wms']);
|
||||
$this->assertDatabaseHas('helpdesk_git_repositories', ['name' => 'WMS Core', 'provider' => 'github']);
|
||||
$this->assertDatabaseHas('helpdesk_workflow_templates', ['name' => 'Ticket Raised', 'event' => 'ticket.created']);
|
||||
}
|
||||
|
||||
public function test_settings_manager_can_edit_and_delete_all_new_setup_records(): void
|
||||
{
|
||||
$admin = $this->admin();
|
||||
$priority = $this->priority(['name' => 'Low', 'slug' => 'low']);
|
||||
$category = $this->category(['name' => 'Support', 'slug' => 'support']);
|
||||
$team = $this->team(['name' => 'First Line', 'slug' => 'first-line']);
|
||||
$project = $this->project(['title' => 'Portal', 'slug' => 'portal', 'team_id' => $team->id]);
|
||||
$repository = $this->repository(['project_id' => $project->id, 'name' => 'Portal Repo']);
|
||||
$template = WorkflowTemplate::query()->create([
|
||||
'name' => 'Closed Message',
|
||||
'event' => 'ticket.closed',
|
||||
'subject' => 'Closed {{ ticket.reference }}',
|
||||
'html_body' => '<p>Closed</p>',
|
||||
'text_body' => 'Closed',
|
||||
'to_role' => 'requester',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$component = Livewire::actingAs($admin)->test(SettingsManager::class);
|
||||
|
||||
$component->call('editPriority', $priority->id)->set('priorityForm.name', 'Low Priority')->call('savePriority');
|
||||
$component->call('editCategory', $category->id)->set('categoryForm.name', 'Customer Support')->call('saveCategory');
|
||||
$component->call('editTeam', $team->id)->set('teamForm.name', 'First Line Support')->call('saveTeam');
|
||||
$component->call('editProject', $project->id)->set('projectForm.title', 'Customer Portal')->call('saveProject');
|
||||
$component->call('editRepository', $repository->id)->set('repositoryForm.name', 'Customer Portal Repo')->call('saveRepository');
|
||||
$component->call('editWorkflowTemplate', $template->id)->set('workflowForm.name', 'Ticket Closed Message')->call('saveWorkflowTemplate');
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_priorities', ['id' => $priority->id, 'name' => 'Low Priority']);
|
||||
$this->assertDatabaseHas('helpdesk_categories', ['id' => $category->id, 'name' => 'Customer Support']);
|
||||
$this->assertDatabaseHas('helpdesk_teams', ['id' => $team->id, 'name' => 'First Line Support']);
|
||||
$this->assertDatabaseHas('helpdesk_projects', ['id' => $project->id, 'title' => 'Customer Portal']);
|
||||
$this->assertDatabaseHas('helpdesk_git_repositories', ['id' => $repository->id, 'name' => 'Customer Portal Repo']);
|
||||
$this->assertDatabaseHas('helpdesk_workflow_templates', ['id' => $template->id, 'name' => 'Ticket Closed Message']);
|
||||
|
||||
$component->call('deletePriority', $priority->id)
|
||||
->call('deleteCategory', $category->id)
|
||||
->call('deleteRepository', $repository->id)
|
||||
->call('deleteWorkflowTemplate', $template->id);
|
||||
|
||||
self::assertNull(Priority::query()->find($priority->id));
|
||||
self::assertNull(Category::query()->find($category->id));
|
||||
self::assertNull(GitRepository::query()->find($repository->id));
|
||||
self::assertNull(WorkflowTemplate::query()->find($template->id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\TicketShow;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TicketConversationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_owner_can_add_public_response_to_ticket(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$owner = $this->user();
|
||||
$ticket = $this->ticket(['owner' => $owner, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
|
||||
Livewire::actingAs($owner)
|
||||
->test(TicketShow::class, ['ticket' => $ticket])
|
||||
->set('commentBody', '<p>Please can you check this update?</p>')
|
||||
->call('addComment')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('helpdesk-comment-added');
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_comments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'parent_id' => null,
|
||||
'user_id' => $owner->id,
|
||||
'is_internal' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_comment_can_have_a_nested_reply(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$owner = $this->user();
|
||||
$ticket = $this->ticket(['owner' => $owner, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
$comment = Comment::query()->create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => $owner->id,
|
||||
'body' => '<p>Parent response</p>',
|
||||
'is_internal' => false,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($owner)
|
||||
->test(TicketShow::class, ['ticket' => $ticket])
|
||||
->call('startReply', $comment->id)
|
||||
->set('replyBody', '<p>Nested reply body</p>')
|
||||
->call('addReply')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('helpdesk-reply-added');
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_comments', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'parent_id' => $comment->id,
|
||||
'body' => '<p>Nested reply body</p>',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_agent_can_update_status_to_closed_and_priority(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$fixtures = $this->seedLookupData();
|
||||
$owner = $this->user();
|
||||
$agent = $this->agent();
|
||||
$ticket = $this->ticket(['owner' => $owner, 'agent' => $agent, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
|
||||
Livewire::actingAs($agent)
|
||||
->test(TicketShow::class, ['ticket' => $ticket])
|
||||
->set('status_id', $fixtures['closed']->id)
|
||||
->call('updateStatus')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('helpdesk-status-updated')
|
||||
->set('priority_id', $fixtures['high']->id)
|
||||
->call('updatePriority')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('helpdesk-priority-updated');
|
||||
|
||||
$ticket->refresh();
|
||||
self::assertNotNull($ticket->completed_at);
|
||||
self::assertSame($fixtures['closed']->id, $ticket->status_id);
|
||||
self::assertSame($fixtures['high']->id, $ticket->priority_id);
|
||||
}
|
||||
|
||||
public function test_unassigned_agent_cannot_update_another_users_ticket(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$owner = $this->user();
|
||||
$agent = $this->agent();
|
||||
$ticket = $this->ticket(['owner' => $owner, 'agent_id' => null, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
|
||||
Livewire::actingAs($agent)
|
||||
->test(TicketShow::class, ['ticket' => $ticket])
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\TicketCreate;
|
||||
use Modules\Helpdesk\Livewire\TicketIndex;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TicketCrudTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_user_can_create_ticket_with_priority_category_team_and_project(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$user = $this->user(['name' => 'Mark Requester']);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(TicketCreate::class)
|
||||
->set('subject', 'Warehouse printer does not print labels')
|
||||
->set('body', '<p>The printer fails every time a dispatch label is generated.</p>')
|
||||
->set('priority_id', $fixtures['high']->id)
|
||||
->set('category_id', $fixtures['category']->id)
|
||||
->set('team_id', $fixtures['team']->id)
|
||||
->set('project_id', $fixtures['project']->id)
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_tickets', [
|
||||
'user_id' => $user->id,
|
||||
'subject' => 'Warehouse printer does not print labels',
|
||||
'priority_id' => $fixtures['high']->id,
|
||||
'category_id' => $fixtures['category']->id,
|
||||
'team_id' => $fixtures['team']->id,
|
||||
'project_id' => $fixtures['project']->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_ticket_create_validates_required_fields(): void
|
||||
{
|
||||
$this->seedLookupData();
|
||||
$user = $this->user();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(TicketCreate::class)
|
||||
->call('save')
|
||||
->assertHasErrors(['subject' => 'required', 'body' => 'required', 'priority_id' => 'required']);
|
||||
}
|
||||
|
||||
public function test_ticket_index_shows_only_visible_tickets_for_standard_user(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$owner = $this->user(['name' => 'Owner User']);
|
||||
$other = $this->user(['name' => 'Other User']);
|
||||
$visible = $this->ticket(['owner' => $owner, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id, 'subject' => 'Visible ticket']);
|
||||
$this->ticket(['owner' => $other, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id, 'subject' => 'Hidden ticket']);
|
||||
|
||||
Livewire::actingAs($owner)
|
||||
->test(TicketIndex::class)
|
||||
->assertSee($visible->reference)
|
||||
->assertSee('Visible ticket')
|
||||
->assertDontSee('Hidden ticket');
|
||||
}
|
||||
|
||||
public function test_agent_sees_assigned_ticket_and_admin_sees_all_tickets(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$agent = $this->agent(['name' => 'Assigned Agent']);
|
||||
$admin = $this->admin(['name' => 'Admin User']);
|
||||
$owner = $this->user();
|
||||
$assigned = $this->ticket(['owner' => $owner, 'agent' => $agent, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id, 'subject' => 'Assigned ticket']);
|
||||
$unassigned = $this->ticket(['owner' => $this->user(), 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id, 'subject' => 'Another ticket']);
|
||||
|
||||
Livewire::actingAs($agent)
|
||||
->test(TicketIndex::class)
|
||||
->assertSee($assigned->reference)
|
||||
->assertDontSee($unassigned->reference);
|
||||
|
||||
Livewire::actingAs($admin)
|
||||
->test(TicketIndex::class)
|
||||
->assertSee($assigned->reference)
|
||||
->assertSee($unassigned->reference);
|
||||
}
|
||||
|
||||
public function test_index_filters_by_search_priority_and_closed_scope(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$admin = $this->admin();
|
||||
$openTicket = $this->ticket(['owner' => $admin, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['high']->id, 'subject' => 'API sync failure']);
|
||||
$closedTicket = $this->ticket(['owner' => $admin, 'status_id' => $fixtures['closed']->id, 'priority_id' => $fixtures['normal']->id, 'completed_at' => now(), 'subject' => 'Old closed issue']);
|
||||
|
||||
Livewire::actingAs($admin)
|
||||
->test(TicketIndex::class)
|
||||
->set('search', 'API sync')
|
||||
->set('priority', (string) $fixtures['high']->id)
|
||||
->assertSee($openTicket->reference)
|
||||
->assertDontSee($closedTicket->reference);
|
||||
|
||||
Livewire::actingAs($admin)
|
||||
->test(TicketIndex::class)
|
||||
->set('scope', 'closed')
|
||||
->assertSee($closedTicket->reference)
|
||||
->assertDontSee($openTicket->reference);
|
||||
}
|
||||
|
||||
public function test_created_ticket_has_a_reference_and_last_activity_date(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$user = $this->user();
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(TicketCreate::class)
|
||||
->set('subject', 'Reference check')
|
||||
->set('body', 'This ticket should have a generated reference.')
|
||||
->set('priority_id', $fixtures['normal']->id)
|
||||
->call('save');
|
||||
|
||||
$ticket = Ticket::query()->where('subject', 'Reference check')->firstOrFail();
|
||||
|
||||
self::assertStringStartsWith('HD-' . now()->format('Y') . '-', $ticket->reference);
|
||||
self::assertNotNull($ticket->last_activity_at);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TicketPolicyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_owner_can_view_and_comment_but_cannot_update_ticket(): void
|
||||
{
|
||||
$owner = $this->user();
|
||||
$ticket = $this->ticket(['owner' => $owner]);
|
||||
|
||||
self::assertTrue(Gate::forUser($owner)->allows('view', $ticket));
|
||||
self::assertTrue(Gate::forUser($owner)->allows('comment', $ticket));
|
||||
self::assertFalse(Gate::forUser($owner)->allows('update', $ticket));
|
||||
}
|
||||
|
||||
public function test_assigned_agent_can_view_comment_and_update_ticket(): void
|
||||
{
|
||||
$owner = $this->user();
|
||||
$agent = $this->agent();
|
||||
$ticket = $this->ticket(['owner' => $owner, 'agent' => $agent]);
|
||||
|
||||
self::assertTrue(Gate::forUser($agent)->allows('view', $ticket));
|
||||
self::assertTrue(Gate::forUser($agent)->allows('comment', $ticket));
|
||||
self::assertTrue(Gate::forUser($agent)->allows('update', $ticket));
|
||||
}
|
||||
|
||||
public function test_helpdesk_admin_can_view_update_and_delete_any_ticket(): void
|
||||
{
|
||||
$admin = $this->admin();
|
||||
$ticket = $this->ticket(['owner' => $this->user()]);
|
||||
|
||||
self::assertTrue(Gate::forUser($admin)->allows('view', $ticket));
|
||||
self::assertTrue(Gate::forUser($admin)->allows('update', $ticket));
|
||||
self::assertTrue(Gate::forUser($admin)->allows('delete', $ticket));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Modules\Helpdesk\Models\WorkflowTemplate;
|
||||
use Modules\Helpdesk\Services\WorkflowNotificationService;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class WorkflowNotificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_workflow_dispatch_sends_active_template_and_logs_it(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$fixtures = $this->seedLookupData();
|
||||
$owner = $this->user(['name' => 'Requester Name', 'email' => 'requester@example.com']);
|
||||
$agent = $this->agent(['name' => 'Agent Name', 'email' => 'agent@example.com']);
|
||||
$ticket = $this->ticket([
|
||||
'owner' => $owner,
|
||||
'agent' => $agent,
|
||||
'status_id' => $fixtures['open']->id,
|
||||
'priority_id' => $fixtures['high']->id,
|
||||
'category_id' => $fixtures['category']->id,
|
||||
'project_id' => $fixtures['project']->id,
|
||||
'team_id' => $fixtures['team']->id,
|
||||
'subject' => 'Workflow ticket subject',
|
||||
]);
|
||||
|
||||
WorkflowTemplate::query()->create([
|
||||
'name' => 'Ticket created requester email',
|
||||
'event' => 'ticket.created',
|
||||
'subject' => 'Ticket {{ ticket.reference }} raised for {{ requester.name }}',
|
||||
'html_body' => '<p>{{ ticket.subject }} / {{ ticket.priority }} / {{ ticket.status }}</p>',
|
||||
'text_body' => '{{ ticket.subject }}',
|
||||
'to_role' => 'requester',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
app(WorkflowNotificationService::class)->dispatch($ticket, 'ticket.created', $agent);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_workflow_logs', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'event' => 'ticket.created',
|
||||
'recipient' => 'requester@example.com',
|
||||
'status' => 'sent',
|
||||
'subject' => 'Ticket ' . $ticket->reference . ' raised for Requester Name',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_inactive_workflow_template_is_not_dispatched(): void
|
||||
{
|
||||
Mail::fake();
|
||||
$ticket = $this->ticket();
|
||||
|
||||
WorkflowTemplate::query()->create([
|
||||
'name' => 'Inactive template',
|
||||
'event' => 'ticket.updated',
|
||||
'subject' => 'Inactive',
|
||||
'html_body' => '<p>Inactive</p>',
|
||||
'text_body' => 'Inactive',
|
||||
'to_role' => 'requester',
|
||||
'is_active' => false,
|
||||
]);
|
||||
|
||||
app(WorkflowNotificationService::class)->dispatch($ticket, 'ticket.updated', $ticket->owner);
|
||||
|
||||
$this->assertDatabaseCount('helpdesk_workflow_logs', 0);
|
||||
}
|
||||
|
||||
public function test_workflow_logs_skipped_when_recipient_is_missing(): void
|
||||
{
|
||||
$ticket = $this->ticket(['agent_id' => null]);
|
||||
|
||||
WorkflowTemplate::query()->create([
|
||||
'name' => 'Agent template',
|
||||
'event' => 'ticket.updated',
|
||||
'subject' => 'Agent update',
|
||||
'html_body' => '<p>Agent update</p>',
|
||||
'text_body' => 'Agent update',
|
||||
'to_role' => 'agent',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
app(WorkflowNotificationService::class)->dispatch($ticket, 'ticket.updated', $ticket->owner);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_workflow_logs', [
|
||||
'ticket_id' => $ticket->id,
|
||||
'event' => 'ticket.updated',
|
||||
'recipient' => null,
|
||||
'status' => 'skipped',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Unit;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Helpdesk\Models\Comment;
|
||||
use Modules\Helpdesk\Models\GitCommit;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class ModelRelationshipTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_ticket_loads_company_project_team_category_priority_status_and_git_commits(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$repository = $this->repository(['project_id' => $fixtures['project']->id]);
|
||||
$ticket = $this->ticket([
|
||||
'status_id' => $fixtures['open']->id,
|
||||
'priority_id' => $fixtures['high']->id,
|
||||
'category_id' => $fixtures['category']->id,
|
||||
'team_id' => $fixtures['team']->id,
|
||||
'project_id' => $fixtures['project']->id,
|
||||
]);
|
||||
|
||||
GitCommit::query()->create([
|
||||
'repository_id' => $repository->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'hash' => str_repeat('a', 40),
|
||||
'short_hash' => 'aaaaaaa',
|
||||
'branch' => 'develop',
|
||||
'author_name' => 'Developer',
|
||||
'author_email' => 'dev@example.com',
|
||||
'message' => 'Fix HD reference handling',
|
||||
'url' => 'https://example.com/commit/a',
|
||||
'committed_at' => now(),
|
||||
]);
|
||||
|
||||
$ticket->refresh()->load(['status', 'priority', 'category', 'team', 'project', 'gitCommits']);
|
||||
|
||||
self::assertSame('Open', $ticket->status?->name);
|
||||
self::assertSame('High', $ticket->priority?->name);
|
||||
self::assertSame('Support', $ticket->category?->name);
|
||||
self::assertSame('Support Team', $ticket->team?->name);
|
||||
self::assertSame('WMS Core', $ticket->project?->title);
|
||||
self::assertCount(1, $ticket->gitCommits);
|
||||
}
|
||||
|
||||
public function test_comments_support_nested_replies(): void
|
||||
{
|
||||
$ticket = $this->ticket();
|
||||
$user = $this->user();
|
||||
$comment = Comment::query()->create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'user_id' => $user->id,
|
||||
'body' => '<p>Parent response</p>',
|
||||
'is_internal' => false,
|
||||
]);
|
||||
Comment::query()->create([
|
||||
'ticket_id' => $ticket->id,
|
||||
'parent_id' => $comment->id,
|
||||
'user_id' => $user->id,
|
||||
'body' => '<p>Nested response</p>',
|
||||
'is_internal' => false,
|
||||
]);
|
||||
|
||||
self::assertSame('Nested response', strip_tags((string) $comment->replies()->firstOrFail()->body));
|
||||
self::assertCount(1, $ticket->comments()->with('replies')->get());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Unit;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Helpdesk\Services\TicketReferenceService;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class TicketReferenceServiceTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
use BuildsHelpdeskFixtures;
|
||||
|
||||
public function test_it_generates_the_first_reference_for_the_current_year(): void
|
||||
{
|
||||
$reference = app(TicketReferenceService::class)->next();
|
||||
|
||||
self::assertSame('HD-' . now()->format('Y') . '-00001', $reference);
|
||||
}
|
||||
|
||||
public function test_it_generates_the_next_reference_after_existing_tickets(): void
|
||||
{
|
||||
$this->ticket(['reference' => 'HD-' . now()->format('Y') . '-00041']);
|
||||
|
||||
$reference = app(TicketReferenceService::class)->next();
|
||||
|
||||
self::assertSame('HD-' . now()->format('Y') . '-00042', $reference);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user