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>
62 lines
1.6 KiB
PHP
62 lines
1.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Monitor\Api\Tests\Sync;
|
|
|
|
use Monitor\Api\Sync\SyncTrigger;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class SyncTriggerTest extends TestCase
|
|
{
|
|
private string $path;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->path = sys_get_temp_dir() . '/monitor-sync-trigger-' . uniqid() . '/sync-requested';
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (file_exists($this->path)) {
|
|
unlink($this->path);
|
|
}
|
|
if (is_dir(dirname($this->path))) {
|
|
rmdir(dirname($this->path));
|
|
}
|
|
}
|
|
|
|
public function testConsumeReportsNothingWhenNoSyncWasRequested(): void
|
|
{
|
|
self::assertFalse((new SyncTrigger($this->path))->consume());
|
|
}
|
|
|
|
public function testConsumeReportsRequestedSyncExactlyOnce(): void
|
|
{
|
|
$trigger = new SyncTrigger($this->path);
|
|
$trigger->request();
|
|
|
|
self::assertTrue($trigger->consume());
|
|
self::assertFalse($trigger->consume());
|
|
}
|
|
|
|
/** Mehrere Webhooks vor dem naechsten Lauf duerfen nicht zu mehreren Syncs fuehren. */
|
|
public function testRepeatedRequestsCollapseIntoOneSync(): void
|
|
{
|
|
$trigger = new SyncTrigger($this->path);
|
|
$trigger->request();
|
|
$trigger->request();
|
|
$trigger->request();
|
|
|
|
self::assertTrue($trigger->consume());
|
|
self::assertFalse($trigger->consume());
|
|
}
|
|
|
|
/** api- und sync-Container teilen sich das Volume, legen die Datei aber ggf. selbst an. */
|
|
public function testRequestCreatesMissingDirectory(): void
|
|
{
|
|
(new SyncTrigger($this->path))->request();
|
|
|
|
self::assertFileExists($this->path);
|
|
}
|
|
}
|