monitor/api/tests/Unit/Controller/WebhookControllerTest.php
fds2610-bot 3efa0ee423 Codeberg-Webhook fuer Sofort-Sync statt Warten auf den Polling-Lauf
Aenderungen an Codeberg-Issues landeten bisher erst beim naechsten Lauf des
sync-Services auf dem Board (MONITOR_SYNC_INTERVAL, Default 600s). Neu meldet
Codeberg jede Aenderung aktiv per Webhook, der Weg aufs Board dauert damit
wenige Sekunden (Webhook -> Sync -> SSE-Push ans Frontend).

POST /api/v1/webhooks/codeberg wertet den Payload bewusst nicht aus, sondern
fordert nur einen regulaeren Sync an - CodebergSyncService bleibt die einzige
Stelle, die Issues auf Tickets abbildet, und ein verpasster Webhook ist
folgenlos, weil der periodische Lauf denselben Stand ohnehin nachholt.

Der Sync passiert nicht im Request: Forgejo bricht die Zustellung nach wenigen
Sekunden ab (DELIVER_TIMEOUT, Default 5s), ein Sync macht aber mehrere Requests
nach codeberg.org. Der Endpunkt setzt daher nur eine Trigger-Datei im
gemeinsamen ./api-Volume (Sync\SyncTrigger) und antwortet sofort mit 202; der
sync-Service laeuft jetzt im Watch-Modus und greift sie im Sekundentakt auf.

Authentifiziert wird per HMAC-SHA256 ueber den rohen Request-Body
(X-Forgejo-Signature bzw. X-Gitea-Signature) - Codeberg kennt unsere API-Keys
nicht. Ohne gesetztes CODEBERG_WEBHOOK_SECRET weist der Endpunkt grundsaetzlich
mit 401 ab, damit ein vergessenes Secret keinen offenen Sync-Ausloeser im
Internet hinterlaesst. Request traegt dafuer neu den unveraenderten rawBody.

Anders als der sync-Service braucht der api-Service damit eine eingehende
Freigabe in Proxy und Firewall (POST /api/v1/webhooks/codeberg).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:04:06 +02:00

79 lines
2.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Monitor\Api\Tests\Controller;
use Monitor\Api\Controller\WebhookController;
use Monitor\Api\Http\Request;
use Monitor\Api\Sync\SyncTrigger;
use PHPUnit\Framework\TestCase;
final class WebhookControllerTest extends TestCase
{
private string $path;
private SyncTrigger $trigger;
protected function setUp(): void
{
$this->path = sys_get_temp_dir() . '/monitor-webhook-' . uniqid() . '/sync-requested';
$this->trigger = new SyncTrigger($this->path);
}
protected function tearDown(): void
{
if (file_exists($this->path)) {
unlink($this->path);
}
if (is_dir(dirname($this->path))) {
rmdir(dirname($this->path));
}
}
public function testIssueEventRequestsASync(): void
{
$controller = new WebhookController($this->trigger);
$response = $controller->codeberg($this->request('issues'));
self::assertSame(202, $response->status);
self::assertTrue($this->trigger->consume());
}
/**
* Codeberg schickt beim Anlegen des Webhooks ein "ping" zum Testen der Zustellung - das
* ist keine Issue-Aenderung und soll keinen Codeberg-Request ausloesen.
*/
public function testPingIsAnsweredWithoutRequestingASync(): void
{
$controller = new WebhookController($this->trigger);
$response = $controller->codeberg($this->request('ping'));
self::assertSame(200, $response->status);
self::assertFalse($this->trigger->consume());
}
/** Unbekannte Events loesen einen Sync aus - lieber einmal zu viel als einen verpassten. */
public function testUnknownEventStillRequestsASync(): void
{
$controller = new WebhookController($this->trigger);
$response = $controller->codeberg($this->request('issue_label'));
self::assertSame(202, $response->status);
self::assertTrue($this->trigger->consume());
}
private function request(string $event): Request
{
return Request::create(
'POST',
'/api/v1/webhooks/codeberg',
[],
[],
['X-Gitea-Event' => $event],
'{}'
);
}
}