diff --git a/src/http/bootstrap.ts b/src/http/bootstrap.ts index 814c445..d7ad7fc 100644 --- a/src/http/bootstrap.ts +++ b/src/http/bootstrap.ts @@ -9,22 +9,20 @@ import { MaintenanceService } from '../services/maintenance'; export async function startServices(_db: Database): Promise { // Exponer la DB globalmente vía locator para servicios que lo usen. try { setDb(_db); } catch {} - await WebhookManager.registerWebhook(); - // Add small delay to allow webhook to propagate - await new Promise(resolve => setTimeout(resolve, 1000)); - const isActive = await WebhookManager.verifyWebhook(); - if (!isActive) { - console.error('❌ Webhook verification failed - retrying in 2 seconds...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - const isActiveRetry = await WebhookManager.verifyWebhook(); - if (!isActiveRetry) { - console.error('❌ Webhook verification failed after retry'); - process.exit(1); - } + + // Iniciar aseguramiento automático del webhook en background (reintentos con backoff) + try { + WebhookManager.startAutoEnsure(); + } catch (e) { + console.error('⚠️ Failed to start webhook auto-ensure loop:', e); } - // Initialize groups - critical for operation - await GroupSyncService.checkInitialGroups(); + // Initialize groups - critical for operation (no abortar si falla) + try { + await GroupSyncService.checkInitialGroups(); + } catch (e) { + console.error('⚠️ Initial groups check failed (will rely on schedulers to recover):', e); + } // Start groups scheduler (periodic sync of groups) try { @@ -73,6 +71,9 @@ export async function startServices(_db: Database): Promise { } export function stopServices(): void { + try { + WebhookManager.stopAutoEnsure(); + } catch {} try { ResponseQueue.stopCleanupScheduler(); } catch {} diff --git a/tests/unit/services/webhook-manager.test.ts b/tests/unit/services/webhook-manager.test.ts index ae23d2d..e88351f 100644 --- a/tests/unit/services/webhook-manager.test.ts +++ b/tests/unit/services/webhook-manager.test.ts @@ -80,4 +80,74 @@ describe('WebhookManager', () => { } }).toThrow(); }); + + test('should retry registration in background until success', async () => { + // Acelerar reintentos + process.env.WEBHOOK_RETRY_INITIAL_DELAY_MS = '10'; + process.env.WEBHOOK_RETRY_MAX_DELAY_MS = '20'; + process.env.WEBHOOK_RETRY_DEGRADED_INTERVAL_MS = '50'; + + const originalFetch: any = globalThis.fetch; + let setCalls = 0; + + const fetchSpy = mock(async (input: any, init?: any) => { + const url = typeof input === 'string' ? input : input.url; + + // Self-test of our own webhook endpoint + if (url === process.env.WEBHOOK_URL) { + return new Response('ok', { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + + // Register webhook endpoint (first fails, then succeeds) + if (typeof url === 'string' && url.includes('/webhook/set/')) { + setCalls++; + if (setCalls < 2) { + return new Response('unavailable', { status: 503, statusText: 'Service Unavailable' }); + } + const body = JSON.stringify({ + id: 'test-id', + url: process.env.WEBHOOK_URL, + enabled: true, + events: ['APPLICATION_STARTUP'] + }); + return new Response(body, { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + + // Verify webhook endpoint (returns enabled) + if (typeof url === 'string' && url.includes('/webhook/find/')) { + const body = JSON.stringify({ + enabled: true, + url: process.env.WEBHOOK_URL, + events: ['APPLICATION_STARTUP'] + }); + return new Response(body, { status: 200, headers: { 'Content-Type': 'application/json' } }); + } + + return new Response('not found', { status: 404 }); + }); + + // @ts-ignore + globalThis.fetch = fetchSpy; + + try { + // Asegurar que no haya un lazo previo corriendo + try { (WebhookManager as any).stopAutoEnsure(); } catch {} + + WebhookManager['startAutoEnsure'](); + + // Esperar a que el lazo logre activar el webhook (o agote timeout) + const deadline = Date.now() + 2000; + while (WebhookManager['autoEnsureActive'] && Date.now() < deadline) { + await new Promise(res => setTimeout(res, 20)); + } + + expect(WebhookManager['autoEnsureActive']).toBe(false); + expect(fetchSpy).toHaveBeenCalled(); + expect(setCalls).toBeGreaterThan(1); + } finally { + try { WebhookManager['stopAutoEnsure'](); } catch {} + // @ts-ignore + globalThis.fetch = originalFetch; + } + }); });