@@ -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