Files
WMS/Modules/Helpdesk/Tests/Feature/GitWebhookIngestionTest.php
T
2026-06-24 16:14:06 +01:00

303 lines
12 KiB
PHP

<?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,
]);
}
public function test_gitea_local_webhook_tracks_latest_push_without_repository_id(): void
{
config()->set('services.gitea.webhook_authorization', 'YWxhZGRpbjpvcGVuc2VzYW1l');
$fixtures = $this->seedLookupData();
$repository = $this->repository([
'provider' => 'gitea',
'project_id' => $fixtures['project']->id,
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
'webhook_secret' => 'gitea-secret',
]);
$ticket = $this->ticket(['project_id' => $fixtures['project']->id]);
$payload = [
'ref' => 'refs/heads/main',
'repository' => [
'name' => 'WMS-Core',
'full_name' => 'Admin/WMS-Core',
'clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
'html_url' => 'http://git.vm:3000/Admin/WMS-Core',
],
'commits' => [
[
'id' => str_repeat('d', 40),
'message' => $ticket->reference . ' Fix local Gitea webhook ingestion',
'url' => 'http://git.vm:3000/Admin/WMS-Core/commit/' . str_repeat('d', 40),
'timestamp' => '2026-06-24T14:20: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.git.gitea.webhook'), [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
'HTTP_AUTHORIZATION' => 'Basic YWxhZGRpbjpvcGVuc2VzYW1l',
'HTTP_X_GITEA_EVENT' => 'push',
'HTTP_X_GITEA_DELIVERY' => 'gitea-delivery-123',
'HTTP_X_GITEA_SIGNATURE' => hash_hmac('sha256', $json, 'gitea-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('d', 40),
'branch' => 'main',
]);
$this->assertDatabaseHas('helpdesk_git_webhook_logs', [
'repository_id' => $repository->id,
'provider' => 'gitea',
'delivery_id' => 'gitea-delivery-123',
'event' => 'push',
'status' => 'accepted',
'linked_count' => 1,
]);
}
public function test_gitea_local_webhook_rejects_invalid_authorization_without_creating_commits(): void
{
config()->set('services.gitea.webhook_authorization', 'YWxhZGRpbjpvcGVuc2VzYW1l');
$this->repository([
'provider' => 'gitea',
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
'webhook_secret' => 'gitea-secret',
]);
$payload = [
'ref' => 'refs/heads/main',
'repository' => ['clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git'],
'commits' => [['id' => str_repeat('e', 40), 'message' => 'No auth']],
];
$json = json_encode($payload, JSON_THROW_ON_ERROR);
$response = $this->call('POST', route('api.git.gitea.webhook'), [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
'HTTP_AUTHORIZATION' => 'Basic invalid',
'HTTP_X_GITEA_EVENT' => 'push',
'HTTP_X_GITEA_SIGNATURE' => hash_hmac('sha256', $json, 'gitea-secret'),
], $json);
$response->assertForbidden()->assertJsonPath('status', 'rejected');
self::assertSame(0, GitCommit::query()->count());
}
public function test_gitea_local_webhook_accepts_prefixed_signature_and_case_insensitive_provider(): void
{
config()->set('services.gitea.webhook_authorization', null);
$fixtures = $this->seedLookupData();
$repository = $this->repository([
'provider' => 'Gitea',
'project_id' => $fixtures['project']->id,
'name' => 'Local',
'remote_url' => 'http://git.vm:3000/Admin/WMS-Core',
'webhook_secret' => 'gitea-secret',
]);
$payload = [
'ref' => 'refs/heads/main',
'repository' => [
'name' => 'WMS-Core',
'full_name' => 'Admin/WMS-Core',
'clone_url' => 'http://git.vm:3000/Admin/WMS-Core.git',
'html_url' => 'http://git.vm:3000/Admin/WMS-Core',
],
'commits' => [
[
'id' => str_repeat('f', 40),
'message' => 'Local Gitea push should be visible on the dashboard',
'url' => 'http://git.vm:3000/Admin/WMS-Core/commit/' . str_repeat('f', 40),
'timestamp' => '2026-06-24T15: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.git.gitea.webhook'), [], [], [], [
'CONTENT_TYPE' => 'application/json',
'HTTP_ACCEPT' => 'application/json',
'HTTP_X_GITEA_EVENT' => 'push',
'HTTP_X_GITEA_SIGNATURE' => 'sha256=' . hash_hmac('sha256', $json, 'gitea-secret'),
], $json);
$response->assertStatus(202)->assertJsonPath('status', 'accepted')->assertJsonPath('commit_count', 1);
$this->assertDatabaseHas('helpdesk_git_commits', [
'repository_id' => $repository->id,
'hash' => str_repeat('f', 40),
'branch' => 'main',
]);
}
}