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' => 'https://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' => 'https://git.vm:3000/Admin/WMS-Core.git', 'html_url' => 'https://git.vm:3000/Admin/WMS-Core', ], 'commits' => [ [ 'id' => str_repeat('d', 40), 'message' => $ticket->reference . ' Fix local Gitea webhook ingestion', 'url' => 'https://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' => 'https://git.vm:3000/Admin/WMS-Core.git', 'webhook_secret' => 'gitea-secret', ]); $payload = [ 'ref' => 'refs/heads/main', 'repository' => ['clone_url' => 'https://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' => 'https://git.vm:3000/Admin/WMS-Core.git', 'webhook_secret' => 'gitea-secret', ]); $payload = [ 'ref' => 'refs/heads/main', 'repository' => ['clone_url' => 'https://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' => 'https://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' => 'https://git.vm:3000/Admin/WMS-Core.git', 'html_url' => 'https://git.vm:3000/Admin/WMS-Core', ], 'commits' => [ [ 'id' => str_repeat('f', 40), 'message' => 'Local Gitea push should be visible on the dashboard', 'url' => 'https://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', ]); } }