334 lines
14 KiB
PHP
334 lines
14 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_signature_and_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, 'wrong-secret'),
|
|
], $json);
|
|
|
|
$response->assertForbidden()->assertJsonPath('status', 'rejected');
|
|
|
|
self::assertSame(0, GitCommit::query()->count());
|
|
}
|
|
|
|
public function test_gitea_local_webhook_accepts_valid_signature_when_optional_authorization_header_differs(): void
|
|
{
|
|
config()->set('services.gitea.webhook_authorization', 'expected-basic-token');
|
|
|
|
$repository = $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('b', 40), 'message' => 'Valid signed Gitea push']],
|
|
];
|
|
$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 something-different-from-gitea-ui',
|
|
'HTTP_X_GITEA_EVENT' => 'push',
|
|
'HTTP_X_GITEA_SIGNATURE' => 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('b', 40),
|
|
]);
|
|
}
|
|
|
|
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',
|
|
]);
|
|
}
|
|
|
|
}
|