Skip to content

Commit c1b1daa

Browse files
author
Your Name
committed
updated with request catcher
1 parent 0e91aa0 commit c1b1daa

2 files changed

Lines changed: 233 additions & 1 deletion

File tree

docker-compose.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ services:
1212
- TESTS_GITHUB_APP_IDENTIFIER
1313
- TESTS_GITHUB_INSTALLATION_ID
1414
- TESTS_GITEA_URL=http://gitea:3000
15+
- TESTS_GITEA_REQUEST_CATCHER_URL=http://request-catcher:5000
1516
depends_on:
1617
gitea:
1718
condition: service_healthy
1819
gitea-bootstrap:
1920
condition: service_completed_successfully
21+
request-catcher:
22+
condition: service_started
2023

2124
gitea:
2225
image: gitea/gitea:1.21.5
@@ -25,6 +28,9 @@ services:
2528
- USER_GID=1000
2629
- GITEA__database__DB_TYPE=sqlite3
2730
- GITEA__security__INSTALL_LOCK=true
31+
- GITEA__webhook__ALLOWED_HOST_LIST=*
32+
- GITEA__webhook__SKIP_TLS_VERIFY=true
33+
- GITEA__webhook__DELIVER_TIMEOUT=10
2834
ports:
2935
- "3000:3000"
3036
volumes:
@@ -54,6 +60,9 @@ services:
5460
TOKEN=$$(su git -c \"gitea admin user generate-access-token --username $$GITEA_ADMIN_USERNAME --token-name $$GITEA_ADMIN_USERNAME-token --scopes all --raw\") &&
5561
echo $$TOKEN > /data/gitea/token.txt
5662
"
57-
63+
request-catcher:
64+
image: appwrite/requestcatcher:1.1.0
65+
ports:
66+
- "5000:5000"
5867
volumes:
5968
gitea-data:

tests/VCS/Adapter/GiteaTest.php

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,102 @@ private function setupGitea(): void
5555
}
5656
}
5757
}
58+
59+
private function configureWebhook(string $owner, string $repositoryName, string $secret): void
60+
{
61+
$catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? '';
62+
$giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? '';
63+
$webhookUrl = $catcherUrl . '/webhook';
64+
65+
$payload = json_encode([
66+
'type' => 'gitea',
67+
'active' => true,
68+
'events' => ['push', 'pull_request'],
69+
'config' => [
70+
'url' => $webhookUrl,
71+
'content_type' => 'json',
72+
'secret' => $secret,
73+
],
74+
]);
75+
76+
$ch = curl_init("{$giteaUrl}/api/v1/repos/{$owner}/{$repositoryName}/hooks");
77+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
78+
curl_setopt($ch, CURLOPT_POST, true);
79+
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
80+
curl_setopt($ch, CURLOPT_HTTPHEADER, [
81+
'Content-Type: application/json',
82+
'Authorization: token ' . self::$accessToken,
83+
]);
84+
curl_exec($ch);
85+
curl_close($ch);
86+
}
87+
88+
89+
/** @return array<mixed> */
90+
private function getLastWebhookRequest(string $eventType = ''): array
91+
{
92+
$catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? '';
93+
94+
if (!empty($eventType)) {
95+
$ch = curl_init("{$catcherUrl}/__find_request__?header_X-Gitea-Event={$eventType}");
96+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
97+
$response = (string) curl_exec($ch);
98+
curl_close($ch);
99+
100+
if (empty($response)) {
101+
return [];
102+
}
103+
104+
$decoded = json_decode($response, true);
105+
106+
if (is_array($decoded) && !empty($decoded)) {
107+
return end($decoded);
108+
}
109+
110+
return [];
111+
}
112+
113+
$ch = curl_init("{$catcherUrl}/__last_request__");
114+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
115+
$response = (string) curl_exec($ch);
116+
curl_close($ch);
117+
118+
if (empty($response)) {
119+
return [];
120+
}
121+
122+
return json_decode($response, true) ?? [];
123+
}
124+
125+
private function assertEventually(callable $probe, int $timeoutMs = 15000, int $waitMs = 500): void
126+
{
127+
$start = microtime(true) * 1000;
128+
$lastException = null;
129+
130+
while ((microtime(true) * 1000 - $start) < $timeoutMs) {
131+
try {
132+
$probe();
133+
return;
134+
} catch (\Throwable $e) {
135+
$lastException = $e;
136+
usleep($waitMs * 1000);
137+
}
138+
}
139+
140+
throw $lastException ?? new \Exception('assertEventually timed out');
141+
}
142+
143+
144+
private function clearWebhookRequests(): void
145+
{
146+
$catcherUrl = System::getEnv('TESTS_GITEA_REQUEST_CATCHER_URL', 'http://request-catcher:5000') ?? '';
147+
148+
$ch = curl_init("{$catcherUrl}/__clear__");
149+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
150+
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
151+
curl_exec($ch);
152+
curl_close($ch);
153+
}
58154
public function testCreateRepository(): void
59155
{
60156
$owner = self::$owner;
@@ -1232,4 +1328,131 @@ public function testListRepositoryLanguagesEmptyRepo(): void
12321328

12331329
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
12341330
}
1331+
1332+
public function testWebhookPushEvent(): void
1333+
{
1334+
$repositoryName = 'test-webhook-push-' . \uniqid();
1335+
$secret = 'test-webhook-secret-' . \uniqid();
1336+
$giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? '';
1337+
1338+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
1339+
1340+
try {
1341+
$this->clearWebhookRequests();
1342+
$this->configureWebhook(self::$owner, $repositoryName, $secret);
1343+
1344+
// Get hook ID to manually trigger delivery
1345+
$ch = curl_init("{$giteaUrl}/api/v1/repos/" . self::$owner . "/{$repositoryName}/hooks");
1346+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1347+
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: token ' . self::$accessToken]);
1348+
$hooksResponse = (string) curl_exec($ch);
1349+
curl_close($ch);
1350+
$hookId = json_decode($hooksResponse, true)[0]['id'] ?? 1;
1351+
1352+
// Trigger a real push by creating a file
1353+
$this->vcsAdapter->createFile(
1354+
self::$owner,
1355+
$repositoryName,
1356+
'README.md',
1357+
'# Webhook Test',
1358+
'Initial commit'
1359+
);
1360+
1361+
// Manually trigger webhook delivery via Gitea API
1362+
$ch = curl_init("{$giteaUrl}/api/v1/repos/" . self::$owner . "/{$repositoryName}/hooks/{$hookId}/tests");
1363+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1364+
curl_setopt($ch, CURLOPT_POST, true);
1365+
curl_setopt($ch, CURLOPT_POSTFIELDS, '');
1366+
curl_setopt($ch, CURLOPT_HTTPHEADER, [
1367+
'Authorization: token ' . self::$accessToken,
1368+
'Content-Type: application/json',
1369+
]);
1370+
curl_exec($ch);
1371+
curl_close($ch);
1372+
1373+
// Wait for push webhook to arrive
1374+
$webhookData = [];
1375+
$this->assertEventually(function () use (&$webhookData) {
1376+
$webhookData = $this->getLastWebhookRequest();
1377+
$this->assertNotEmpty($webhookData, 'No webhook received');
1378+
$this->assertNotEmpty($webhookData['data'] ?? '', 'Webhook payload is empty');
1379+
$this->assertSame('push', $webhookData['headers']['X-Gitea-Event'] ?? '', 'Expected push event');
1380+
});
1381+
1382+
$payload = $webhookData['data'];
1383+
$headers = $webhookData['headers'] ?? [];
1384+
$signature = $headers['X-Gitea-Signature'] ?? '';
1385+
1386+
$this->assertNotEmpty($signature, 'Missing X-Gitea-Signature header');
1387+
$this->assertTrue(
1388+
$this->vcsAdapter->validateWebhookEvent($payload, $signature, $secret),
1389+
'Webhook signature validation failed'
1390+
);
1391+
1392+
$event = $this->vcsAdapter->getEvent('push', $payload);
1393+
$this->assertIsArray($event);
1394+
$this->assertSame('main', $event['branch']);
1395+
$this->assertSame($repositoryName, $event['repositoryName']);
1396+
$this->assertSame(self::$owner, $event['owner']);
1397+
$this->assertNotEmpty($event['commitHash']);
1398+
} finally {
1399+
$this->clearWebhookRequests();
1400+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
1401+
}
1402+
}
1403+
1404+
public function testWebhookPullRequestEvent(): void
1405+
{
1406+
$repositoryName = 'test-webhook-pr-' . \uniqid();
1407+
$secret = 'test-webhook-secret-' . \uniqid();
1408+
1409+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
1410+
1411+
try {
1412+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
1413+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-branch', 'main');
1414+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'feature.txt', 'content', 'Add feature', 'feature-branch');
1415+
1416+
$this->configureWebhook(self::$owner, $repositoryName, $secret);
1417+
$this->clearWebhookRequests();
1418+
1419+
$this->vcsAdapter->createPullRequest(
1420+
self::$owner,
1421+
$repositoryName,
1422+
'Test Webhook PR',
1423+
'feature-branch',
1424+
'main'
1425+
);
1426+
1427+
$webhookData = [];
1428+
$this->assertEventually(function () use (&$webhookData) {
1429+
$webhookData = $this->getLastWebhookRequest('pull_request');
1430+
$this->assertNotEmpty($webhookData, 'No pull_request webhook received');
1431+
$this->assertNotEmpty($webhookData['data'] ?? '', 'Webhook payload is empty');
1432+
});
1433+
1434+
$payload = $webhookData['data'];
1435+
$headers = $webhookData['headers'] ?? [];
1436+
$signature = $headers['X-Gitea-Signature'] ?? '';
1437+
1438+
$this->assertNotEmpty($signature, 'Missing X-Gitea-Signature header');
1439+
$this->assertTrue(
1440+
$this->vcsAdapter->validateWebhookEvent($payload, $signature, $secret),
1441+
'Webhook signature validation failed'
1442+
);
1443+
1444+
$event = $this->vcsAdapter->getEvent('pull_request', $payload);
1445+
1446+
$this->assertIsArray($event);
1447+
$this->assertSame('feature-branch', $event['branch']);
1448+
$this->assertSame($repositoryName, $event['repositoryName']);
1449+
$this->assertSame(self::$owner, $event['owner']);
1450+
$this->assertContains($event['action'], ['opened', 'synchronized']);
1451+
$this->assertGreaterThan(0, $event['pullRequestNumber']);
1452+
} finally {
1453+
$this->clearWebhookRequests();
1454+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
1455+
}
1456+
}
1457+
12351458
}

0 commit comments

Comments
 (0)