From 1b88c391255d16bf22ae17f314c81b347ed8341d Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 20 Mar 2026 16:47:27 +0530 Subject: [PATCH 1/6] feat: implement Gitea webhook support - Implement getEvent() for push and pull_request events - Implement validateWebhookEvent() with HMAC-SHA256 validation - Parse webhook payloads to match GitHub adapter format - Add comprehensive test coverage for all webhook scenarios - Test push events with affected files tracking - Test pull request events including external/fork detection - Test signature validation (valid and invalid cases) - Test error handling for invalid payloads and unsupported events - Add abstract method stub for PHPStan compatibility Resolves webhook functionality for Gitea adapter --- src/VCS/Adapter/Git/Gitea.php | 135 ++++++++++++++++++- tests/VCS/Adapter/GiteaTest.php | 227 +++++++++++++++++++++++++++++++- 2 files changed, 359 insertions(+), 3 deletions(-) diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index aedf6c7e..7e92e4e0 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -739,13 +739,144 @@ public function generateCloneCommand(string $owner, string $repositoryName, stri throw new Exception("Not implemented yet"); } + /** + * Parses webhook event payload + * + * @param string $event Type of event: push, pull_request, etc + * @param string $payload The webhook payload received from Gitea + * @return array Parsed payload as an array + */ public function getEvent(string $event, string $payload): array { - throw new Exception("Not implemented yet"); + $payload = json_decode($payload, true); + + if ($payload === null || !is_array($payload)) { + throw new Exception("Invalid payload."); + } + + switch ($event) { + case 'push': + $payloadRepository = $payload['repository'] ?? []; + $payloadRepositoryOwner = $payloadRepository['owner'] ?? []; + $payloadSender = $payload['sender'] ?? []; + $payloadHeadCommit = $payload['head_commit'] ?? []; + $payloadHeadCommitAuthor = $payloadHeadCommit['author'] ?? []; + + $branchCreated = $payload['created'] ?? false; + $branchDeleted = $payload['deleted'] ?? false; + $repositoryId = strval($payloadRepository['id'] ?? ''); + $repositoryName = $payloadRepository['name'] ?? ''; + $branch = str_replace('refs/heads/', '', $payload['ref'] ?? ''); + $repositoryUrl = $payloadRepository['html_url'] ?? ''; + $branchUrl = !empty($repositoryUrl) && !empty($branch) ? $repositoryUrl . "/src/branch/" . $branch : ''; + $commitHash = $payload['after'] ?? ''; + $owner = $payloadRepositoryOwner['login'] ?? ''; + $authorUrl = $payloadSender['html_url'] ?? ''; + $authorAvatarUrl = $payloadSender['avatar_url'] ?? ''; + $headCommitAuthorName = $payloadHeadCommitAuthor['name'] ?? ''; + $headCommitAuthorEmail = $payloadHeadCommitAuthor['email'] ?? ''; + $headCommitMessage = $payloadHeadCommit['message'] ?? ''; + $headCommitUrl = $payloadHeadCommit['url'] ?? ''; + + $affectedFiles = []; + foreach (($payload['commits'] ?? []) as $commit) { + foreach (($commit['added'] ?? []) as $added) { + $affectedFiles[$added] = true; + } + + foreach (($commit['removed'] ?? []) as $removed) { + $affectedFiles[$removed] = true; + } + + foreach (($commit['modified'] ?? []) as $modified) { + $affectedFiles[$modified] = true; + } + } + + return [ + 'branchCreated' => $branchCreated, + 'branchDeleted' => $branchDeleted, + 'branch' => $branch, + 'branchUrl' => $branchUrl, + 'repositoryId' => $repositoryId, + 'repositoryName' => $repositoryName, + 'repositoryUrl' => $repositoryUrl, + 'installationId' => '', // Gitea doesn't have installations + 'commitHash' => $commitHash, + 'owner' => $owner, + 'authorUrl' => $authorUrl, + 'authorAvatarUrl' => $authorAvatarUrl, + 'headCommitAuthorName' => $headCommitAuthorName, + 'headCommitAuthorEmail' => $headCommitAuthorEmail, + 'headCommitMessage' => $headCommitMessage, + 'headCommitUrl' => $headCommitUrl, + 'external' => false, + 'pullRequestNumber' => '', + 'action' => '', + 'affectedFiles' => \array_keys($affectedFiles), + ]; + + case 'pull_request': + $payloadRepository = $payload['repository'] ?? []; + $payloadRepositoryOwner = $payloadRepository['owner'] ?? []; + $payloadSender = $payload['sender'] ?? []; + $payloadPullRequest = $payload['pull_request'] ?? []; + $payloadPullRequestHead = $payloadPullRequest['head'] ?? []; + $payloadPullRequestHeadRepo = $payloadPullRequestHead['repo'] ?? []; + $payloadPullRequestHeadUser = $payloadPullRequestHead['user'] ?? []; + $payloadPullRequestUser = $payloadPullRequest['user'] ?? []; + $payloadPullRequestBase = $payloadPullRequest['base'] ?? []; + $payloadPullRequestBaseUser = $payloadPullRequestBase['user'] ?? []; + + $repositoryId = strval($payloadRepository['id'] ?? ''); + $branch = $payloadPullRequestHead['ref'] ?? ''; + $repositoryName = $payloadRepository['name'] ?? ''; + $repositoryUrl = $payloadRepository['html_url'] ?? ''; + $branchUrl = !empty($repositoryUrl) && !empty($branch) ? $repositoryUrl . "/src/branch/" . $branch : ''; + $pullRequestNumber = strval($payload['number'] ?? ''); + $action = $payload['action'] ?? ''; + $owner = $payloadRepositoryOwner['login'] ?? ''; + $authorUrl = $payloadSender['html_url'] ?? ''; + $authorAvatarUrl = $payloadPullRequestUser['avatar_url'] ?? ''; + $commitHash = $payloadPullRequestHead['sha'] ?? ''; + $headCommitUrl = $repositoryUrl ? $repositoryUrl . "/commit/" . $commitHash : ''; + + // Check if PR is from a fork (external) + $headRepoFullName = $payloadPullRequestHeadRepo['full_name'] ?? ''; + $baseRepoFullName = $payloadRepository['full_name'] ?? ''; + $external = !empty($headRepoFullName) && !empty($baseRepoFullName) && $headRepoFullName !== $baseRepoFullName; + + return [ + 'branch' => $branch, + 'branchUrl' => $branchUrl, + 'repositoryId' => $repositoryId, + 'repositoryName' => $repositoryName, + 'repositoryUrl' => $repositoryUrl, + 'installationId' => '', // Gitea doesn't have installations + 'commitHash' => $commitHash, + 'owner' => $owner, + 'authorUrl' => $authorUrl, + 'authorAvatarUrl' => $authorAvatarUrl, + 'headCommitUrl' => $headCommitUrl, + 'external' => $external, + 'pullRequestNumber' => $pullRequestNumber, + 'action' => $action, + ]; + } + + return []; } + /** + * Validate webhook event + * + * @param string $payload Raw body of HTTP request + * @param string $signature Signature provided by Gitea in X-Gitea-Signature header + * @param string $signatureKey Webhook secret configured on Gitea + * @return bool + */ public function validateWebhookEvent(string $payload, string $signature, string $signatureKey): bool { - throw new Exception("Not implemented yet"); + return $signature === hash_hmac('sha256', $payload, $signatureKey); } } diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 88e75341..88699a03 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -643,8 +643,233 @@ public function testGetLatestCommitWithInvalidBranch(): void public function testGetEvent(): void { - $this->markTestSkipped('Will be implemented in follow-up PR'); + // Base class requires this method - implemented via specific event tests below + $this->assertTrue(true); + } + + public function testGetEventPush(): void + { + $payload = json_encode([ + 'ref' => 'refs/heads/main', + 'before' => 'abc123', + 'after' => 'def456', + 'created' => false, + 'deleted' => false, + 'repository' => [ + 'id' => 123, + 'name' => 'test-repo', + 'html_url' => 'http://gitea:3000/test-owner/test-repo', + 'owner' => [ + 'login' => 'test-owner', + ], + ], + 'sender' => [ + 'login' => 'pusher-user', + 'html_url' => 'http://gitea:3000/pusher-user', + 'avatar_url' => 'http://gitea:3000/avatars/pusher', + ], + 'head_commit' => [ + 'id' => 'def456', + 'message' => 'Test commit message', + 'url' => 'http://gitea:3000/test-owner/test-repo/commit/def456', + 'author' => [ + 'name' => 'Test Author', + 'email' => 'author@example.com', + ], + ], + 'commits' => [ + [ + 'id' => 'def456', + 'added' => ['file1.txt'], + 'removed' => ['file2.txt'], + 'modified' => ['file3.txt'], + ], + ], + ]); + + if ($payload === false) { + $this->fail('Failed to encode JSON payload'); + } + + $result = $this->vcsAdapter->getEvent('push', $payload); + + $this->assertIsArray($result); + $this->assertArrayHasKey('branch', $result); + $this->assertArrayHasKey('commitHash', $result); + $this->assertArrayHasKey('repositoryName', $result); + $this->assertArrayHasKey('owner', $result); + $this->assertArrayHasKey('affectedFiles', $result); + + $this->assertSame('main', $result['branch']); + $this->assertSame('def456', $result['commitHash']); + $this->assertSame('test-repo', $result['repositoryName']); + $this->assertSame('test-owner', $result['owner']); + $this->assertSame('Test commit message', $result['headCommitMessage']); + $this->assertSame('Test Author', $result['headCommitAuthorName']); + $this->assertSame('author@example.com', $result['headCommitAuthorEmail']); + + $this->assertIsArray($result['affectedFiles']); + $this->assertContains('file1.txt', $result['affectedFiles']); + $this->assertContains('file2.txt', $result['affectedFiles']); + $this->assertContains('file3.txt', $result['affectedFiles']); + } + + public function testGetEventPullRequest(): void + { + $payload = json_encode([ + 'action' => 'opened', + 'number' => 42, + 'pull_request' => [ + 'id' => 1, + 'number' => 42, + 'state' => 'open', + 'title' => 'Test PR', + 'head' => [ + 'ref' => 'feature-branch', + 'sha' => 'abc123', + 'repo' => [ + 'full_name' => 'test-owner/test-repo', + ], + 'user' => [ + 'login' => 'pr-author', + ], + ], + 'base' => [ + 'ref' => 'main', + 'sha' => 'def456', + 'user' => [ + 'login' => 'base-owner', + ], + ], + 'user' => [ + 'login' => 'pr-author', + 'avatar_url' => 'http://gitea:3000/avatars/pr-author', + ], + ], + 'repository' => [ + 'id' => 123, + 'name' => 'test-repo', + 'full_name' => 'test-owner/test-repo', + 'html_url' => 'http://gitea:3000/test-owner/test-repo', + 'owner' => [ + 'login' => 'test-owner', + ], + ], + 'sender' => [ + 'login' => 'sender-user', + 'html_url' => 'http://gitea:3000/sender-user', + ], + ]); + + if ($payload === false) { + $this->fail('Failed to encode JSON payload'); + } + + $result = $this->vcsAdapter->getEvent('pull_request', $payload); + + $this->assertIsArray($result); + $this->assertArrayHasKey('branch', $result); + $this->assertArrayHasKey('pullRequestNumber', $result); + $this->assertArrayHasKey('action', $result); + $this->assertArrayHasKey('commitHash', $result); + $this->assertArrayHasKey('external', $result); + + $this->assertSame('feature-branch', $result['branch']); + $this->assertSame('42', $result['pullRequestNumber']); + $this->assertSame('opened', $result['action']); + $this->assertSame('abc123', $result['commitHash']); + $this->assertSame('test-repo', $result['repositoryName']); + $this->assertSame('test-owner', $result['owner']); + $this->assertFalse($result['external']); + } + + public function testGetEventPullRequestExternal(): void + { + $payload = json_encode([ + 'action' => 'opened', + 'number' => 42, + 'pull_request' => [ + 'head' => [ + 'ref' => 'feature-branch', + 'sha' => 'abc123', + 'repo' => [ + 'full_name' => 'external-user/forked-repo', + ], + ], + 'base' => [ + 'ref' => 'main', + ], + 'user' => [ + 'avatar_url' => 'http://gitea:3000/avatars/external', + ], + ], + 'repository' => [ + 'id' => 123, + 'name' => 'test-repo', + 'full_name' => 'test-owner/test-repo', + 'html_url' => 'http://gitea:3000/test-owner/test-repo', + 'owner' => [ + 'login' => 'test-owner', + ], + ], + 'sender' => [ + 'html_url' => 'http://gitea:3000/external-user', + ], + ]); + + if ($payload === false) { + $this->fail('Failed to encode JSON payload'); + } + + $result = $this->vcsAdapter->getEvent('pull_request', $payload); + + $this->assertTrue($result['external']); + } + + public function testValidateWebhookEvent(): void + { + $payload = 'test payload content'; + $secret = 'my-webhook-secret'; + $validSignature = hash_hmac('sha256', $payload, $secret); + + $result = $this->vcsAdapter->validateWebhookEvent($payload, $validSignature, $secret); + + $this->assertTrue($result); } + + public function testValidateWebhookEventInvalid(): void + { + $payload = 'test payload content'; + $secret = 'my-webhook-secret'; + $invalidSignature = 'wrong-signature'; + + $result = $this->vcsAdapter->validateWebhookEvent($payload, $invalidSignature, $secret); + + $this->assertFalse($result); + } + + public function testGetEventInvalidPayload(): void + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Invalid payload'); + + $this->vcsAdapter->getEvent('push', 'invalid json'); + } + + public function testGetEventUnsupportedEvent(): void + { + $payload = json_encode(['test' => 'data']); + + if ($payload === false) { + $this->fail('Failed to encode JSON payload'); + } + + $result = $this->vcsAdapter->getEvent('unsupported_event', $payload); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + public function testSearchRepositories(): void { // Create multiple repositories From 99041df81685d4b2212f8312b09233de3664a14c Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 20 Mar 2026 17:57:16 +0530 Subject: [PATCH 2/6] updated with suggestions --- src/VCS/Adapter/Git/Gitea.php | 6 ++---- tests/VCS/Adapter/GiteaTest.php | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index 7e92e4e0..1dc3b894 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -823,17 +823,15 @@ public function getEvent(string $event, string $payload): array $payloadPullRequest = $payload['pull_request'] ?? []; $payloadPullRequestHead = $payloadPullRequest['head'] ?? []; $payloadPullRequestHeadRepo = $payloadPullRequestHead['repo'] ?? []; - $payloadPullRequestHeadUser = $payloadPullRequestHead['user'] ?? []; $payloadPullRequestUser = $payloadPullRequest['user'] ?? []; $payloadPullRequestBase = $payloadPullRequest['base'] ?? []; - $payloadPullRequestBaseUser = $payloadPullRequestBase['user'] ?? []; $repositoryId = strval($payloadRepository['id'] ?? ''); $branch = $payloadPullRequestHead['ref'] ?? ''; $repositoryName = $payloadRepository['name'] ?? ''; $repositoryUrl = $payloadRepository['html_url'] ?? ''; $branchUrl = !empty($repositoryUrl) && !empty($branch) ? $repositoryUrl . "/src/branch/" . $branch : ''; - $pullRequestNumber = strval($payload['number'] ?? ''); + $pullRequestNumber = $payload['number'] ?? ''; $action = $payload['action'] ?? ''; $owner = $payloadRepositoryOwner['login'] ?? ''; $authorUrl = $payloadSender['html_url'] ?? ''; @@ -877,6 +875,6 @@ public function getEvent(string $event, string $payload): array */ public function validateWebhookEvent(string $payload, string $signature, string $signatureKey): bool { - return $signature === hash_hmac('sha256', $payload, $signatureKey); + return hash_equals($signature, hash_hmac('sha256', $payload, $signatureKey)); } } diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 88699a03..b691258d 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -775,7 +775,7 @@ public function testGetEventPullRequest(): void $this->assertArrayHasKey('external', $result); $this->assertSame('feature-branch', $result['branch']); - $this->assertSame('42', $result['pullRequestNumber']); + $this->assertSame(42, $result['pullRequestNumber']); $this->assertSame('opened', $result['action']); $this->assertSame('abc123', $result['commitHash']); $this->assertSame('test-repo', $result['repositoryName']); From 0e91aa09182a01720097332ee68b3ce9cca63f03 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 21 Mar 2026 10:32:42 +0530 Subject: [PATCH 3/6] updated githubtests file with the same patter as giteatest in webhook and removed the testgetevent --- tests/VCS/Adapter/GitHubTest.php | 52 ++++++++++++++++++-------------- tests/VCS/Adapter/GiteaTest.php | 6 ---- tests/VCS/Base.php | 2 -- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index 9038fcee..a518656a 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -26,9 +26,9 @@ public function setUp(): void $this->vcsAdapter->initializeVariables(installationId: $installationId, privateKey: $privateKey, appId: $appId, accessToken: '', refreshToken: ''); } - public function testGetEvent(): void + public function testGetEventPush(): void { - $payload_push = '{ + $payload = '{ "created": false, "ref": "refs/heads/main", "before": "1234", @@ -91,7 +91,19 @@ public function testGetEvent(): void } }'; - $payload_pull_request = '{ + $result = $this->vcsAdapter->getEvent('push', $payload); + + $this->assertSame('main', $result['branch']); + $this->assertSame('603754812', $result['repositoryId']); + $this->assertCount(3, $result['affectedFiles']); + $this->assertSame('src/lib.js', $result['affectedFiles'][0]); + $this->assertSame('README.md', $result['affectedFiles'][1]); + $this->assertSame('src/main.js', $result['affectedFiles'][2]); + } + + public function testGetEventPullRequest(): void + { + $payload = '{ "action": "opened", "number": 1, "pull_request": { @@ -133,7 +145,15 @@ public function testGetEvent(): void } }'; - $payload_uninstall = '{ + $result = $this->vcsAdapter->getEvent('pull_request', $payload); + + $this->assertSame('opened', $result['action']); + $this->assertSame(1, $result['pullRequestNumber']); + } + + public function testGetEventInstallation(): void + { + $payload = '{ "action": "deleted", "installation": { "id": 1234, @@ -141,24 +161,12 @@ public function testGetEvent(): void "login": "vermakhushboo" } } - } - '; - - $pushResult = $this->vcsAdapter->getEvent('push', $payload_push); - $this->assertSame('main', $pushResult['branch']); - $this->assertSame('603754812', $pushResult['repositoryId']); - $this->assertCount(3, $pushResult['affectedFiles']); - $this->assertSame('src/lib.js', $pushResult['affectedFiles'][0]); - $this->assertSame('README.md', $pushResult['affectedFiles'][1]); - $this->assertSame('src/main.js', $pushResult['affectedFiles'][2]); - - $pullRequestResult = $this->vcsAdapter->getEvent('pull_request', $payload_pull_request); - $this->assertSame('opened', $pullRequestResult['action']); - $this->assertSame(1, $pullRequestResult['pullRequestNumber']); - - $uninstallResult = $this->vcsAdapter->getEvent('installation', $payload_uninstall); - $this->assertSame('deleted', $uninstallResult['action']); - $this->assertSame('1234', $uninstallResult['installationId']); + }'; + + $result = $this->vcsAdapter->getEvent('installation', $payload); + + $this->assertSame('deleted', $result['action']); + $this->assertSame('1234', $result['installationId']); } public function testGetComment(): void diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index b691258d..9ebea159 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -641,12 +641,6 @@ public function testGetLatestCommitWithInvalidBranch(): void } } - public function testGetEvent(): void - { - // Base class requires this method - implemented via specific event tests below - $this->assertTrue(true); - } - public function testGetEventPush(): void { $payload = json_encode([ diff --git a/tests/VCS/Base.php b/tests/VCS/Base.php index cb3f1db7..e6b23aab 100644 --- a/tests/VCS/Base.php +++ b/tests/VCS/Base.php @@ -25,8 +25,6 @@ abstract public function testGenerateCloneCommand(): void; abstract public function testGenerateCloneCommandWithCommitHash(): void; - abstract public function testGetEvent(): void; - abstract public function testGetRepositoryName(): void; abstract public function testGetComment(): void; From c1b1daad4e6bf2da05c6f8760f7d1107f7457fef Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 21 Mar 2026 14:20:06 +0530 Subject: [PATCH 4/6] updated with request catcher --- docker-compose.yml | 11 +- tests/VCS/Adapter/GiteaTest.php | 223 ++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 95f4099a..c42da29b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,11 +12,14 @@ services: - TESTS_GITHUB_APP_IDENTIFIER - TESTS_GITHUB_INSTALLATION_ID - TESTS_GITEA_URL=http://gitea:3000 + - TESTS_GITEA_REQUEST_CATCHER_URL=http://request-catcher:5000 depends_on: gitea: condition: service_healthy gitea-bootstrap: condition: service_completed_successfully + request-catcher: + condition: service_started gitea: image: gitea/gitea:1.21.5 @@ -25,6 +28,9 @@ services: - USER_GID=1000 - GITEA__database__DB_TYPE=sqlite3 - GITEA__security__INSTALL_LOCK=true + - GITEA__webhook__ALLOWED_HOST_LIST=* + - GITEA__webhook__SKIP_TLS_VERIFY=true + - GITEA__webhook__DELIVER_TIMEOUT=10 ports: - "3000:3000" volumes: @@ -54,6 +60,9 @@ services: TOKEN=$$(su git -c \"gitea admin user generate-access-token --username $$GITEA_ADMIN_USERNAME --token-name $$GITEA_ADMIN_USERNAME-token --scopes all --raw\") && echo $$TOKEN > /data/gitea/token.txt " - + request-catcher: + image: appwrite/requestcatcher:1.1.0 + ports: + - "5000:5000" volumes: gitea-data: \ No newline at end of file diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 9ebea159..74202869 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -55,6 +55,102 @@ private function setupGitea(): void } } } + + private function configureWebhook(string $owner, string $repositoryName, string $secret): void + { + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + $giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? ''; + $webhookUrl = $catcherUrl . '/webhook'; + + $payload = json_encode([ + 'type' => 'gitea', + 'active' => true, + 'events' => ['push', 'pull_request'], + 'config' => [ + 'url' => $webhookUrl, + 'content_type' => 'json', + 'secret' => $secret, + ], + ]); + + $ch = curl_init("{$giteaUrl}/api/v1/repos/{$owner}/{$repositoryName}/hooks"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: token ' . self::$accessToken, + ]); + curl_exec($ch); + curl_close($ch); + } + + + /** @return array */ + private function getLastWebhookRequest(string $eventType = ''): array + { + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + + if (!empty($eventType)) { + $ch = curl_init("{$catcherUrl}/__find_request__?header_X-Gitea-Event={$eventType}"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = (string) curl_exec($ch); + curl_close($ch); + + if (empty($response)) { + return []; + } + + $decoded = json_decode($response, true); + + if (is_array($decoded) && !empty($decoded)) { + return end($decoded); + } + + return []; + } + + $ch = curl_init("{$catcherUrl}/__last_request__"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $response = (string) curl_exec($ch); + curl_close($ch); + + if (empty($response)) { + return []; + } + + return json_decode($response, true) ?? []; + } + + private function assertEventually(callable $probe, int $timeoutMs = 15000, int $waitMs = 500): void + { + $start = microtime(true) * 1000; + $lastException = null; + + while ((microtime(true) * 1000 - $start) < $timeoutMs) { + try { + $probe(); + return; + } catch (\Throwable $e) { + $lastException = $e; + usleep($waitMs * 1000); + } + } + + throw $lastException ?? new \Exception('assertEventually timed out'); + } + + + private function clearWebhookRequests(): void + { + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + + $ch = curl_init("{$catcherUrl}/__clear__"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_exec($ch); + curl_close($ch); + } public function testCreateRepository(): void { $owner = self::$owner; @@ -1232,4 +1328,131 @@ public function testListRepositoryLanguagesEmptyRepo(): void $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); } + + public function testWebhookPushEvent(): void + { + $repositoryName = 'test-webhook-push-' . \uniqid(); + $secret = 'test-webhook-secret-' . \uniqid(); + $giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? ''; + + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + try { + $this->clearWebhookRequests(); + $this->configureWebhook(self::$owner, $repositoryName, $secret); + + // Get hook ID to manually trigger delivery + $ch = curl_init("{$giteaUrl}/api/v1/repos/" . self::$owner . "/{$repositoryName}/hooks"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: token ' . self::$accessToken]); + $hooksResponse = (string) curl_exec($ch); + curl_close($ch); + $hookId = json_decode($hooksResponse, true)[0]['id'] ?? 1; + + // Trigger a real push by creating a file + $this->vcsAdapter->createFile( + self::$owner, + $repositoryName, + 'README.md', + '# Webhook Test', + 'Initial commit' + ); + + // Manually trigger webhook delivery via Gitea API + $ch = curl_init("{$giteaUrl}/api/v1/repos/" . self::$owner . "/{$repositoryName}/hooks/{$hookId}/tests"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, ''); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: token ' . self::$accessToken, + 'Content-Type: application/json', + ]); + curl_exec($ch); + curl_close($ch); + + // Wait for push webhook to arrive + $webhookData = []; + $this->assertEventually(function () use (&$webhookData) { + $webhookData = $this->getLastWebhookRequest(); + $this->assertNotEmpty($webhookData, 'No webhook received'); + $this->assertNotEmpty($webhookData['data'] ?? '', 'Webhook payload is empty'); + $this->assertSame('push', $webhookData['headers']['X-Gitea-Event'] ?? '', 'Expected push event'); + }); + + $payload = $webhookData['data']; + $headers = $webhookData['headers'] ?? []; + $signature = $headers['X-Gitea-Signature'] ?? ''; + + $this->assertNotEmpty($signature, 'Missing X-Gitea-Signature header'); + $this->assertTrue( + $this->vcsAdapter->validateWebhookEvent($payload, $signature, $secret), + 'Webhook signature validation failed' + ); + + $event = $this->vcsAdapter->getEvent('push', $payload); + $this->assertIsArray($event); + $this->assertSame('main', $event['branch']); + $this->assertSame($repositoryName, $event['repositoryName']); + $this->assertSame(self::$owner, $event['owner']); + $this->assertNotEmpty($event['commitHash']); + } finally { + $this->clearWebhookRequests(); + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + } + + public function testWebhookPullRequestEvent(): void + { + $repositoryName = 'test-webhook-pr-' . \uniqid(); + $secret = 'test-webhook-secret-' . \uniqid(); + + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + try { + $this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test'); + $this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-branch', 'main'); + $this->vcsAdapter->createFile(self::$owner, $repositoryName, 'feature.txt', 'content', 'Add feature', 'feature-branch'); + + $this->configureWebhook(self::$owner, $repositoryName, $secret); + $this->clearWebhookRequests(); + + $this->vcsAdapter->createPullRequest( + self::$owner, + $repositoryName, + 'Test Webhook PR', + 'feature-branch', + 'main' + ); + + $webhookData = []; + $this->assertEventually(function () use (&$webhookData) { + $webhookData = $this->getLastWebhookRequest('pull_request'); + $this->assertNotEmpty($webhookData, 'No pull_request webhook received'); + $this->assertNotEmpty($webhookData['data'] ?? '', 'Webhook payload is empty'); + }); + + $payload = $webhookData['data']; + $headers = $webhookData['headers'] ?? []; + $signature = $headers['X-Gitea-Signature'] ?? ''; + + $this->assertNotEmpty($signature, 'Missing X-Gitea-Signature header'); + $this->assertTrue( + $this->vcsAdapter->validateWebhookEvent($payload, $signature, $secret), + 'Webhook signature validation failed' + ); + + $event = $this->vcsAdapter->getEvent('pull_request', $payload); + + $this->assertIsArray($event); + $this->assertSame('feature-branch', $event['branch']); + $this->assertSame($repositoryName, $event['repositoryName']); + $this->assertSame(self::$owner, $event['owner']); + $this->assertContains($event['action'], ['opened', 'synchronized']); + $this->assertGreaterThan(0, $event['pullRequestNumber']); + } finally { + $this->clearWebhookRequests(); + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + } + } From 3668a5a45c2bcd11d0dc890c5c13ffb27ea3eed4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 22 Mar 2026 19:28:49 +0530 Subject: [PATCH 5/6] updated with automatic trigger --- composer.json | 3 +- composer.lock | 48 +++++++++++-- docker-compose.yml | 1 + src/VCS/Adapter/Git.php | 13 ++++ src/VCS/Adapter/Git/GitHub.php | 10 +++ src/VCS/Adapter/Git/Gitea.php | 37 ++++++++++ tests/VCS/Adapter/GiteaTest.php | 118 ++++++++++---------------------- 7 files changed, 143 insertions(+), 87 deletions(-) diff --git a/composer.json b/composer.json index 00d46355..86e23a33 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,8 @@ "require": { "php": ">=8.0", "adhocore/jwt": "^1.1", - "utopia-php/cache": "1.0.*" + "utopia-php/cache": "1.0.*", + "utopia-php/fetch": "1.0.*" }, "require-dev": { "phpunit/phpunit": "^9.4", diff --git a/composer.lock b/composer.lock index 4643220a..b0861589 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "53c307bde5f5612420f5359faeea0b92", + "content-hash": "c852a00fe5b6a9a36484ffc38940afcd", "packages": [ { "name": "adhocore/jwt", @@ -1980,6 +1980,46 @@ }, "time": "2026-03-12T03:39:09+00:00" }, + { + "name": "utopia-php/fetch", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/fetch.git", + "reference": "89760880428d869d9da0ebc45bab207befda4c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/fetch/zipball/89760880428d869d9da0ebc45bab207befda4c4d", + "reference": "89760880428d869d9da0ebc45bab207befda4c4d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "laravel/pint": "^1.5.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5", + "swoole/ide-helper": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\Fetch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A simple library that provides an interface for making HTTP Requests.", + "support": { + "issues": "https://github.com/utopia-php/fetch/issues", + "source": "https://github.com/utopia-php/fetch/tree/1.0.0" + }, + "time": "2026-01-19T09:50:33+00:00" + }, { "name": "utopia-php/pools", "version": "1.0.3", @@ -4066,12 +4106,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": {}, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.0" }, - "platform-dev": {}, - "plugin-api-version": "2.9.0" + "platform-dev": [], + "plugin-api-version": "2.6.0" } diff --git a/docker-compose.yml b/docker-compose.yml index c42da29b..77a3aa8e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: - GITEA__webhook__ALLOWED_HOST_LIST=* - GITEA__webhook__SKIP_TLS_VERIFY=true - GITEA__webhook__DELIVER_TIMEOUT=10 + - GITEA__server__LOCAL_ROOT_URL=http://gitea:3000/ ports: - "3000:3000" volumes: diff --git a/src/VCS/Adapter/Git.php b/src/VCS/Adapter/Git.php index 6a4a7edf..5ce6c439 100644 --- a/src/VCS/Adapter/Git.php +++ b/src/VCS/Adapter/Git.php @@ -70,4 +70,17 @@ abstract public function createBranch(string $owner, string $repositoryName, str * @return array Created PR details */ abstract public function createPullRequest(string $owner, string $repositoryName, string $title, string $head, string $base, string $body = ''): array; + + /** + * Create a webhook on a repository + * + * @param string $owner Owner of the repository + * @param string $repositoryName Name of the repository + * @param string $url Webhook URL to send events to + * @param string $secret Webhook secret for signature validation + * @param array $events Events to trigger the webhook + * @return int Webhook ID + */ + abstract public function createWebhook(string $owner, string $repositoryName, string $url, string $secret, array $events = ['push', 'pull_request']): int; + } diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 958cac09..2ac02d56 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -109,6 +109,16 @@ public function createPullRequest(string $owner, string $repositoryName, string throw new Exception('Not implemented'); } + /** + * Create a webhook on a repository + * + * Note: Not applicable for GitHub - webhooks are managed via GitHub Apps + */ + public function createWebhook(string $owner, string $repositoryName, string $url, string $secret, array $events = ['push', 'pull_request']): int + { + throw new Exception('Not applicable for GitHub - webhooks are managed via GitHub Apps'); + } + /** * Create a file in a repository * diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index 1dc3b894..1b8f4ba5 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -473,6 +473,43 @@ public function createPullRequest(string $owner, string $repositoryName, string return $responseBody; } + /** + * Create a webhook on a repository + * + * @param string $owner Owner of the repository + * @param string $repositoryName Name of the repository + * @param string $url Webhook URL to send events to + * @param string $secret Webhook secret for signature validation + * @param array $events Events to trigger the webhook + * @return int Webhook ID + */ + public function createWebhook(string $owner, string $repositoryName, string $url, string $secret, array $events = ['push', 'pull_request']): int + { + $response = $this->call( + self::METHOD_POST, + "/repos/{$owner}/{$repositoryName}/hooks", + ['Authorization' => "token $this->accessToken"], + [ + 'type' => 'gitea', + 'active' => true, + 'events' => $events, + 'config' => [ + 'url' => $url, + 'content_type' => 'json', + 'secret' => $secret, + ], + ] + ); + + $responseHeaders = $response['headers'] ?? []; + $responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0; + if ($responseHeadersStatusCode >= 400) { + throw new Exception("Failed to create webhook: HTTP {$responseHeadersStatusCode}"); + } + + return (int) ($response['body']['id'] ?? 0); + } + public function createComment(string $owner, string $repositoryName, int $pullRequestNumber, string $comment): string { $url = "/repos/{$owner}/{$repositoryName}/issues/{$pullRequestNumber}/comments"; diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 74202869..5c03aa27 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -8,6 +8,7 @@ use Utopia\Tests\Base; use Utopia\VCS\Adapter\Git; use Utopia\VCS\Adapter\Git\Gitea; +use Utopia\Fetch\Client; class GiteaTest extends Base { @@ -56,70 +57,40 @@ private function setupGitea(): void } } - private function configureWebhook(string $owner, string $repositoryName, string $secret): void + private function configureWebhook(string $owner, string $repositoryName, string $secret): int { $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; - $giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? ''; - $webhookUrl = $catcherUrl . '/webhook'; - $payload = json_encode([ - 'type' => 'gitea', - 'active' => true, - 'events' => ['push', 'pull_request'], - 'config' => [ - 'url' => $webhookUrl, - 'content_type' => 'json', - 'secret' => $secret, - ], - ]); - - $ch = curl_init("{$giteaUrl}/api/v1/repos/{$owner}/{$repositoryName}/hooks"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Content-Type: application/json', - 'Authorization: token ' . self::$accessToken, - ]); - curl_exec($ch); - curl_close($ch); + return $this->vcsAdapter->createWebhook( + $owner, + $repositoryName, + $catcherUrl . '/webhook', + $secret + ); } - /** @return array */ - private function getLastWebhookRequest(string $eventType = ''): array + private function getLastWebhookRequest(): array { $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; - if (!empty($eventType)) { - $ch = curl_init("{$catcherUrl}/__find_request__?header_X-Gitea-Event={$eventType}"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = (string) curl_exec($ch); - curl_close($ch); - - if (empty($response)) { - return []; - } - - $decoded = json_decode($response, true); - - if (is_array($decoded) && !empty($decoded)) { - return end($decoded); - } + $client = new Client(); + $response = $client->fetch( + url: "{$catcherUrl}/__last_request__", + method: 'GET' + ); + if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { return []; } - $ch = curl_init("{$catcherUrl}/__last_request__"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $response = (string) curl_exec($ch); - curl_close($ch); + $body = $response->text(); - if (empty($response)) { + if (empty($body)) { return []; } - return json_decode($response, true) ?? []; + return json_decode($body, true) ?? []; } private function assertEventually(callable $probe, int $timeoutMs = 15000, int $waitMs = 500): void @@ -140,17 +111,17 @@ private function assertEventually(callable $probe, int $timeoutMs = 15000, int $ throw $lastException ?? new \Exception('assertEventually timed out'); } - private function clearWebhookRequests(): void { $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; - $ch = curl_init("{$catcherUrl}/__clear__"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - curl_exec($ch); - curl_close($ch); + $client = new Client(); + $client->fetch( + url: "{$catcherUrl}/__clear__", + method: 'DELETE' + ); } + public function testCreateRepository(): void { $owner = self::$owner; @@ -1333,7 +1304,6 @@ public function testWebhookPushEvent(): void { $repositoryName = 'test-webhook-push-' . \uniqid(); $secret = 'test-webhook-secret-' . \uniqid(); - $giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? ''; $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); @@ -1341,14 +1311,6 @@ public function testWebhookPushEvent(): void $this->clearWebhookRequests(); $this->configureWebhook(self::$owner, $repositoryName, $secret); - // Get hook ID to manually trigger delivery - $ch = curl_init("{$giteaUrl}/api/v1/repos/" . self::$owner . "/{$repositoryName}/hooks"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: token ' . self::$accessToken]); - $hooksResponse = (string) curl_exec($ch); - curl_close($ch); - $hookId = json_decode($hooksResponse, true)[0]['id'] ?? 1; - // Trigger a real push by creating a file $this->vcsAdapter->createFile( self::$owner, @@ -1358,26 +1320,14 @@ public function testWebhookPushEvent(): void 'Initial commit' ); - // Manually trigger webhook delivery via Gitea API - $ch = curl_init("{$giteaUrl}/api/v1/repos/" . self::$owner . "/{$repositoryName}/hooks/{$hookId}/tests"); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, ''); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: token ' . self::$accessToken, - 'Content-Type: application/json', - ]); - curl_exec($ch); - curl_close($ch); - - // Wait for push webhook to arrive + // Wait for push webhook to arrive automatically $webhookData = []; $this->assertEventually(function () use (&$webhookData) { $webhookData = $this->getLastWebhookRequest(); $this->assertNotEmpty($webhookData, 'No webhook received'); $this->assertNotEmpty($webhookData['data'] ?? '', 'Webhook payload is empty'); $this->assertSame('push', $webhookData['headers']['X-Gitea-Event'] ?? '', 'Expected push event'); - }); + }, 15000, 500); $payload = $webhookData['data']; $headers = $webhookData['headers'] ?? []; @@ -1396,7 +1346,6 @@ public function testWebhookPushEvent(): void $this->assertSame(self::$owner, $event['owner']); $this->assertNotEmpty($event['commitHash']); } finally { - $this->clearWebhookRequests(); $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); } } @@ -1409,13 +1358,18 @@ public function testWebhookPullRequestEvent(): void $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); try { + // Create all files BEFORE configuring webhook + // so those push events don't pollute the catcher $this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test'); $this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-branch', 'main'); $this->vcsAdapter->createFile(self::$owner, $repositoryName, 'feature.txt', 'content', 'Add feature', 'feature-branch'); $this->configureWebhook(self::$owner, $repositoryName, $secret); + + // Clear after setup so only PR event will arrive $this->clearWebhookRequests(); + // Trigger real PR event $this->vcsAdapter->createPullRequest( self::$owner, $repositoryName, @@ -1424,12 +1378,14 @@ public function testWebhookPullRequestEvent(): void 'main' ); + // Wait for pull_request webhook to arrive automatically $webhookData = []; $this->assertEventually(function () use (&$webhookData) { - $webhookData = $this->getLastWebhookRequest('pull_request'); - $this->assertNotEmpty($webhookData, 'No pull_request webhook received'); + $webhookData = $this->getLastWebhookRequest(); + $this->assertNotEmpty($webhookData, 'No webhook received'); $this->assertNotEmpty($webhookData['data'] ?? '', 'Webhook payload is empty'); - }); + $this->assertSame('pull_request', $webhookData['headers']['X-Gitea-Event'] ?? '', 'Expected pull_request event'); + }, 15000, 500); $payload = $webhookData['data']; $headers = $webhookData['headers'] ?? []; @@ -1442,7 +1398,6 @@ public function testWebhookPullRequestEvent(): void ); $event = $this->vcsAdapter->getEvent('pull_request', $payload); - $this->assertIsArray($event); $this->assertSame('feature-branch', $event['branch']); $this->assertSame($repositoryName, $event['repositoryName']); @@ -1450,7 +1405,6 @@ public function testWebhookPullRequestEvent(): void $this->assertContains($event['action'], ['opened', 'synchronized']); $this->assertGreaterThan(0, $event['pullRequestNumber']); } finally { - $this->clearWebhookRequests(); $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); } } From 92cbe1941838a8926cc28d220f92e4592f2e2685 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 23 Mar 2026 15:37:05 +0530 Subject: [PATCH 6/6] updated with quality suggestion --- tests/VCS/Adapter/GiteaTest.php | 76 +++------------------------------ tests/VCS/Base.php | 54 +++++++++++++++++++++++ 2 files changed, 60 insertions(+), 70 deletions(-) diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 5c03aa27..e9be9676 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -8,7 +8,6 @@ use Utopia\Tests\Base; use Utopia\VCS\Adapter\Git; use Utopia\VCS\Adapter\Git\Gitea; -use Utopia\Fetch\Client; class GiteaTest extends Base { @@ -57,71 +56,6 @@ private function setupGitea(): void } } - private function configureWebhook(string $owner, string $repositoryName, string $secret): int - { - $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; - - return $this->vcsAdapter->createWebhook( - $owner, - $repositoryName, - $catcherUrl . '/webhook', - $secret - ); - } - - /** @return array */ - private function getLastWebhookRequest(): array - { - $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; - - $client = new Client(); - $response = $client->fetch( - url: "{$catcherUrl}/__last_request__", - method: 'GET' - ); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { - return []; - } - - $body = $response->text(); - - if (empty($body)) { - return []; - } - - return json_decode($body, true) ?? []; - } - - private function assertEventually(callable $probe, int $timeoutMs = 15000, int $waitMs = 500): void - { - $start = microtime(true) * 1000; - $lastException = null; - - while ((microtime(true) * 1000 - $start) < $timeoutMs) { - try { - $probe(); - return; - } catch (\Throwable $e) { - $lastException = $e; - usleep($waitMs * 1000); - } - } - - throw $lastException ?? new \Exception('assertEventually timed out'); - } - - private function clearWebhookRequests(): void - { - $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; - - $client = new Client(); - $client->fetch( - url: "{$catcherUrl}/__clear__", - method: 'DELETE' - ); - } - public function testCreateRepository(): void { $owner = self::$owner; @@ -1308,8 +1242,9 @@ public function testWebhookPushEvent(): void $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); try { - $this->clearWebhookRequests(); - $this->configureWebhook(self::$owner, $repositoryName, $secret); + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + $this->deleteLastWebhookRequest(); + $this->vcsAdapter->createWebhook(self::$owner, $repositoryName, $catcherUrl . '/webhook', $secret); // Trigger a real push by creating a file $this->vcsAdapter->createFile( @@ -1364,10 +1299,11 @@ public function testWebhookPullRequestEvent(): void $this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-branch', 'main'); $this->vcsAdapter->createFile(self::$owner, $repositoryName, 'feature.txt', 'content', 'Add feature', 'feature-branch'); - $this->configureWebhook(self::$owner, $repositoryName, $secret); + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + $this->vcsAdapter->createWebhook(self::$owner, $repositoryName, $catcherUrl . '/webhook', $secret); // Clear after setup so only PR event will arrive - $this->clearWebhookRequests(); + $this->deleteLastWebhookRequest(); // Trigger real PR event $this->vcsAdapter->createPullRequest( diff --git a/tests/VCS/Base.php b/tests/VCS/Base.php index e6b23aab..8e17c8cc 100644 --- a/tests/VCS/Base.php +++ b/tests/VCS/Base.php @@ -4,6 +4,7 @@ use Exception; use PHPUnit\Framework\TestCase; +use Utopia\Fetch\Client; use Utopia\System\System; use Utopia\VCS\Adapter\Git; use Utopia\VCS\Adapter\Git\GitHub; @@ -33,6 +34,59 @@ abstract public function testGetPullRequest(): void; abstract public function testGetRepositoryTree(): void; + /** @return array */ + protected function getLastWebhookRequest(): array + { + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + + $client = new Client(); + $response = $client->fetch( + url: "{$catcherUrl}/__last_request__", + method: 'GET' + ); + + if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { + return []; + } + + $body = $response->text(); + + if (empty($body)) { + return []; + } + + return json_decode($body, true) ?? []; + } + + protected function assertEventually(callable $probe, int $timeoutMs = 15000, int $waitMs = 500): void + { + $start = microtime(true) * 1000; + $lastException = null; + + while ((microtime(true) * 1000 - $start) < $timeoutMs) { + try { + $probe(); + return; + } catch (\Throwable $e) { + $lastException = $e; + usleep($waitMs * 1000); + } + } + + throw $lastException ?? new \Exception('assertEventually timed out'); + } + + protected function deleteLastWebhookRequest(): void + { + $catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? ''; + + $client = new Client(); + $client->fetch( + url: "{$catcherUrl}/__clear__", + method: 'DELETE' + ); + } + public function testGetPullRequestFromBranch(): void { $result = $this->vcsAdapter->getPullRequestFromBranch('vermakhushboo', 'basic-js-crud', 'test');