major helpdesk updates
This commit is contained in:
@@ -31,12 +31,36 @@ final class HelpdeskSettingsBrowserTest extends DuskTestCase
|
||||
->assertSee('Projects')
|
||||
->assertSee('Git')
|
||||
->assertSee('Workflow')
|
||||
->assertSee('Canned')
|
||||
->assertSee('SLA')
|
||||
->assertSee('Rules')
|
||||
->assertSee('Checks')
|
||||
->assertSee('Tags')
|
||||
->assertSee('Products')
|
||||
->press('Git')
|
||||
->waitForText('Git repositories')
|
||||
->assertSee('Remote URL')
|
||||
->press('Workflow')
|
||||
->waitForText('Workflow messages')
|
||||
->assertSee('HTML email body');
|
||||
->assertSee('HTML email body')
|
||||
->press('Canned')
|
||||
->waitForText('Canned responses')
|
||||
->assertSee('Reply body')
|
||||
->press('SLA')
|
||||
->waitForText('SLA policies')
|
||||
->assertSee('First response minutes')
|
||||
->press('Rules')
|
||||
->waitForText('Email automation rules')
|
||||
->assertSee('Subject contains')
|
||||
->press('Checks')
|
||||
->waitForText('Checklist templates')
|
||||
->assertSee('Checklist items')
|
||||
->press('Tags')
|
||||
->waitForText('Ticket tags')
|
||||
->assertSee('Colour')
|
||||
->press('Products')
|
||||
->waitForText('Products and services')
|
||||
->assertSee('Release/build');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Helpdesk\Models\GitCommit;
|
||||
use Modules\Helpdesk\Models\GitWebhookLog;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class GitWebhookIngestionTest extends TestCase
|
||||
{
|
||||
use BuildsHelpdeskFixtures;
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_signed_github_push_webhook_tracks_and_links_commit_to_ticket_reference(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$repository = $this->repository([
|
||||
'provider' => 'github',
|
||||
'project_id' => $fixtures['project']->id,
|
||||
'webhook_secret' => 'github-secret',
|
||||
]);
|
||||
$ticket = $this->ticket(['project_id' => $fixtures['project']->id]);
|
||||
$payload = [
|
||||
'ref' => 'refs/heads/feature/'.$ticket->reference.'-automation',
|
||||
'commits' => [
|
||||
[
|
||||
'id' => str_repeat('a', 40),
|
||||
'message' => $ticket->reference.' Add webhook ingestion',
|
||||
'url' => 'https://github.example.test/org/repo/commit/'.str_repeat('a', 40),
|
||||
'timestamp' => '2026-06-15T10:00:00+00:00',
|
||||
'author' => ['name' => 'Mark Gregory', 'email' => 'mark@example.com'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.helpdesk.git.webhook', ['repository' => $repository->id]), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_X_GITHUB_EVENT' => 'push',
|
||||
'HTTP_X_GITHUB_DELIVERY' => 'delivery-123',
|
||||
'HTTP_X_HUB_SIGNATURE_256' => 'sha256='.hash_hmac('sha256', $json, 'github-secret'),
|
||||
], $json);
|
||||
|
||||
$response->assertStatus(202)->assertJsonPath('status', 'accepted')->assertJsonPath('linked_count', 1);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_commits', [
|
||||
'repository_id' => $repository->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'hash' => str_repeat('a', 40),
|
||||
'short_hash' => str_repeat('a', 12),
|
||||
'branch' => 'feature/'.$ticket->reference.'-automation',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_webhook_logs', [
|
||||
'repository_id' => $repository->id,
|
||||
'provider' => 'github',
|
||||
'delivery_id' => 'delivery-123',
|
||||
'event' => 'push',
|
||||
'status' => 'accepted',
|
||||
'commit_count' => 1,
|
||||
'linked_count' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_github_webhook_rejects_invalid_signature_without_creating_commits(): void
|
||||
{
|
||||
$repository = $this->repository(['provider' => 'github', 'webhook_secret' => 'github-secret']);
|
||||
$payload = ['ref' => 'refs/heads/develop', 'commits' => [['id' => str_repeat('b', 40), 'message' => 'No ticket']]];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.helpdesk.git.webhook', ['repository' => $repository->id]), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_X_GITHUB_EVENT' => 'push',
|
||||
'HTTP_X_HUB_SIGNATURE_256' => 'sha256=invalid',
|
||||
], $json);
|
||||
|
||||
$response->assertForbidden()->assertJsonPath('status', 'rejected');
|
||||
|
||||
self::assertSame(0, GitCommit::query()->count());
|
||||
self::assertSame('rejected', GitWebhookLog::query()->firstOrFail()->status);
|
||||
}
|
||||
|
||||
public function test_github_non_push_webhook_is_audited_and_ignored(): void
|
||||
{
|
||||
$repository = $this->repository(['provider' => 'github', 'webhook_secret' => 'github-secret']);
|
||||
$payload = ['zen' => 'Keep it logically awesome.'];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.helpdesk.git.webhook', ['repository' => $repository->id]), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_X_GITHUB_EVENT' => 'ping',
|
||||
'HTTP_X_HUB_SIGNATURE_256' => 'sha256='.hash_hmac('sha256', $json, 'github-secret'),
|
||||
], $json);
|
||||
|
||||
$response->assertStatus(202)->assertJsonPath('status', 'ignored');
|
||||
|
||||
self::assertSame(0, GitCommit::query()->count());
|
||||
$this->assertDatabaseHas('helpdesk_git_webhook_logs', [
|
||||
'repository_id' => $repository->id,
|
||||
'provider' => 'github',
|
||||
'event' => 'ping',
|
||||
'status' => 'ignored',
|
||||
'ignored_count' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_gitlab_push_webhook_tracks_and_links_commit_to_ticket_reference(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$repository = $this->repository([
|
||||
'provider' => 'gitlab',
|
||||
'project_id' => $fixtures['project']->id,
|
||||
'webhook_secret' => 'gitlab-secret',
|
||||
]);
|
||||
$ticket = $this->ticket(['project_id' => $fixtures['project']->id]);
|
||||
$payload = [
|
||||
'ref' => 'refs/heads/develop',
|
||||
'commits' => [
|
||||
[
|
||||
'id' => str_repeat('c', 40),
|
||||
'message' => 'Fix '.$ticket->reference.' SLA report',
|
||||
'url' => 'https://gitlab.example.test/org/repo/-/commit/'.str_repeat('c', 40),
|
||||
'timestamp' => '2026-06-15T11:00:00+00:00',
|
||||
'author' => ['name' => 'Support Agent', 'email' => 'agent@example.com'],
|
||||
],
|
||||
],
|
||||
];
|
||||
$json = json_encode($payload, JSON_THROW_ON_ERROR);
|
||||
|
||||
$response = $this->call('POST', route('api.helpdesk.git.webhook', ['repository' => $repository->id]), [], [], [], [
|
||||
'CONTENT_TYPE' => 'application/json',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
'HTTP_X_GITLAB_EVENT' => 'Push Hook',
|
||||
'HTTP_X_GITLAB_TOKEN' => 'gitlab-secret',
|
||||
'HTTP_X_GITLAB_EVENT_UUID' => 'gitlab-delivery-123',
|
||||
], $json);
|
||||
|
||||
$response->assertStatus(202)->assertJsonPath('status', 'accepted')->assertJsonPath('linked_count', 1);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_commits', [
|
||||
'repository_id' => $repository->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'hash' => str_repeat('c', 40),
|
||||
'branch' => 'develop',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('helpdesk_git_webhook_logs', [
|
||||
'repository_id' => $repository->id,
|
||||
'provider' => 'gitlab',
|
||||
'delivery_id' => 'gitlab-delivery-123',
|
||||
'event' => 'Push Hook',
|
||||
'status' => 'accepted',
|
||||
'linked_count' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Helpdesk\Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Modules\Helpdesk\Livewire\TicketShow;
|
||||
use Modules\Helpdesk\Models\AutomationRule;
|
||||
use Modules\Helpdesk\Models\CannedResponse;
|
||||
use Modules\Helpdesk\Models\ChecklistItem;
|
||||
use Modules\Helpdesk\Models\ChecklistTemplate;
|
||||
use Modules\Helpdesk\Models\Product;
|
||||
use Modules\Helpdesk\Models\SlaPolicy;
|
||||
use Modules\Helpdesk\Models\Ticket;
|
||||
use Modules\Helpdesk\Models\TicketSlaEvent;
|
||||
use Modules\Helpdesk\Services\AutomationRuleService;
|
||||
use Modules\Helpdesk\Services\ChecklistService;
|
||||
use Modules\Helpdesk\Services\SlaService;
|
||||
use Modules\Helpdesk\Services\TicketService;
|
||||
use Modules\Helpdesk\Tests\Concerns\BuildsHelpdeskFixtures;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class RedmineInspiredHelpdeskFeatureTest extends TestCase
|
||||
{
|
||||
use BuildsHelpdeskFixtures;
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_ticket_creation_applies_automation_sla_checklist_and_product_linkage(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$user = $this->user(['name' => 'Mark Requester']);
|
||||
$product = Product::query()->create(['name' => 'WMS Platform', 'slug' => 'wms-platform', 'type' => 'service', 'is_active' => true]);
|
||||
|
||||
SlaPolicy::query()->create([
|
||||
'name' => 'High Priority SLA',
|
||||
'slug' => 'high-priority-sla',
|
||||
'priority_id' => $fixtures['high']->id,
|
||||
'first_response_minutes' => 30,
|
||||
'resolution_minutes' => 240,
|
||||
'is_default' => true,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
AutomationRule::query()->create([
|
||||
'name' => 'Printer escalation',
|
||||
'slug' => 'printer-escalation',
|
||||
'match_subject' => 'printer',
|
||||
'set_priority_id' => $fixtures['high']->id,
|
||||
'set_team_id' => $fixtures['team']->id,
|
||||
'is_active' => true,
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
ChecklistTemplate::query()->create([
|
||||
'name' => 'Bug checklist',
|
||||
'slug' => 'bug-checklist',
|
||||
'category_id' => $fixtures['category']->id,
|
||||
'items' => ['Confirm reproduction', 'Record workaround'],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$ticket = app(TicketService::class)->create($user, [
|
||||
'subject' => 'Printer labels are failing',
|
||||
'body' => '<p>Printer labels fail for every order.</p>',
|
||||
'priority_id' => $fixtures['normal']->id,
|
||||
'category_id' => $fixtures['category']->id,
|
||||
'project_id' => $fixtures['project']->id,
|
||||
'product_id' => $product->id,
|
||||
]);
|
||||
|
||||
$ticket = $ticket->fresh(['checklistItems']);
|
||||
|
||||
self::assertSame($fixtures['high']->id, $ticket->priority_id);
|
||||
self::assertSame($fixtures['team']->id, $ticket->team_id);
|
||||
self::assertSame($product->id, $ticket->product_id);
|
||||
self::assertNotNull($ticket->sla_policy_id);
|
||||
self::assertNotNull($ticket->sla_first_response_due_at);
|
||||
self::assertCount(2, $ticket->checklistItems);
|
||||
$this->assertDatabaseHas('helpdesk_ticket_sla_events', ['ticket_id' => $ticket->id, 'event' => 'sla.policy_applied']);
|
||||
}
|
||||
|
||||
public function test_canned_response_macros_insert_into_ticket_reply_editor(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$agent = $this->agent(['name' => 'Agent Smith']);
|
||||
$ticket = $this->ticket(['owner' => $this->user(['name' => 'Customer Jane']), 'agent' => $agent, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
$response = CannedResponse::query()->create([
|
||||
'name' => 'Acknowledgement',
|
||||
'slug' => 'acknowledgement',
|
||||
'body' => 'Hello {{ requester.name }}, ticket {{ ticket.reference }} is being reviewed by {{ agent.name }}.',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($agent)
|
||||
->test(TicketShow::class, ['ticket' => $ticket])
|
||||
->set('canned_response_id', $response->id)
|
||||
->call('applyCannedResponse')
|
||||
->assertSet('commentBody', 'Hello Customer Jane, ticket '.$ticket->reference.' is being reviewed by Agent Smith.');
|
||||
}
|
||||
|
||||
public function test_sla_service_records_first_response_and_resolution_breaches(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$agent = $this->agent();
|
||||
$ticket = $this->ticket(['status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['high']->id]);
|
||||
|
||||
$ticket->forceFill([
|
||||
'sla_first_response_due_at' => now()->subMinute(),
|
||||
'sla_resolution_due_at' => now()->subMinute(),
|
||||
])->save();
|
||||
|
||||
app(SlaService::class)->recordPublicResponse($ticket->fresh(), $agent);
|
||||
app(SlaService::class)->recordClosure($ticket->fresh());
|
||||
|
||||
$ticket = $ticket->fresh();
|
||||
|
||||
self::assertNotNull($ticket->first_response_at);
|
||||
self::assertNotNull($ticket->sla_first_response_breached_at);
|
||||
self::assertNotNull($ticket->sla_resolution_breached_at);
|
||||
self::assertGreaterThanOrEqual(2, TicketSlaEvent::query()->where('ticket_id', $ticket->id)->count());
|
||||
}
|
||||
|
||||
public function test_checklist_items_can_be_toggled_by_agents(): void
|
||||
{
|
||||
$fixtures = $this->seedLookupData();
|
||||
$agent = $this->agent();
|
||||
$ticket = $this->ticket(['agent' => $agent, 'status_id' => $fixtures['open']->id, 'priority_id' => $fixtures['normal']->id]);
|
||||
$item = ChecklistItem::query()->create(['ticket_id' => $ticket->id, 'title' => 'Confirm reproduction', 'sort_order' => 10]);
|
||||
|
||||
$updated = app(ChecklistService::class)->toggle($item, $agent);
|
||||
|
||||
self::assertTrue($updated->is_completed);
|
||||
self::assertSame($agent->id, $updated->completed_by);
|
||||
self::assertNotNull($updated->completed_at);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user