"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const crypto_1 = require("crypto"); const Integration_1 = require("../domain/entities/Integration"); const IntegrationType_1 = require("../domain/value-objects/IntegrationType"); const WebhookEndpoint_1 = require("../domain/entities/WebhookEndpoint"); const WebhookSecret_1 = require("../domain/value-objects/WebhookSecret"); const WebhookDispatcher_1 = require("../infrastructure/webhooks/WebhookDispatcher"); const pino_1 = require("pino"); // ─── Integration Entity ─────────────────────────────────────────────────────── (0, vitest_1.describe)('Integration', () => { (0, vitest_1.it)('creates with defaults', () => { const integration = Integration_1.Integration.create({ name: 'My Slack', type: IntegrationType_1.IntegrationType.slack(), config: { webhookUrl: 'https://hooks.slack.com/test' }, }); (0, vitest_1.expect)(integration.name).toBe('My Slack'); (0, vitest_1.expect)(integration.type.value).toBe('slack'); (0, vitest_1.expect)(integration.enabled).toBe(true); (0, vitest_1.expect)(integration.config.webhookUrl).toBe('https://hooks.slack.com/test'); }); (0, vitest_1.it)('enable and disable', () => { const integration = Integration_1.Integration.create({ name: 'Test', type: IntegrationType_1.IntegrationType.github(), config: {}, }); integration.disable(); (0, vitest_1.expect)(integration.enabled).toBe(false); integration.enable(); (0, vitest_1.expect)(integration.enabled).toBe(true); }); (0, vitest_1.it)('updateConfig merges config', () => { const integration = Integration_1.Integration.create({ name: 'Jira', type: IntegrationType_1.IntegrationType.jira(), config: { host: 'https://old.atlassian.net' }, }); integration.updateConfig({ host: 'https://new.atlassian.net', token: 'tok' }); (0, vitest_1.expect)(integration.config.host).toBe('https://new.atlassian.net'); (0, vitest_1.expect)(integration.config.token).toBe('tok'); }); }); // ─── IntegrationType ────────────────────────────────────────────────────────── (0, vitest_1.describe)('IntegrationType', () => { (0, vitest_1.it)('parses all valid types', () => { (0, vitest_1.expect)(IntegrationType_1.IntegrationType.fromString('slack').value).toBe('slack'); (0, vitest_1.expect)(IntegrationType_1.IntegrationType.fromString('github').value).toBe('github'); (0, vitest_1.expect)(IntegrationType_1.IntegrationType.fromString('jira').value).toBe('jira'); (0, vitest_1.expect)(IntegrationType_1.IntegrationType.fromString('webhook').value).toBe('webhook'); }); (0, vitest_1.it)('throws on invalid type', () => { (0, vitest_1.expect)(() => IntegrationType_1.IntegrationType.fromString('unknown')).toThrow(); }); }); // ─── WebhookEndpoint ────────────────────────────────────────────────────────── (0, vitest_1.describe)('WebhookEndpoint', () => { (0, vitest_1.it)('creates with auto-generated secret', () => { const endpoint = WebhookEndpoint_1.WebhookEndpoint.create({ url: 'https://example.com/hook' }); (0, vitest_1.expect)(endpoint.url).toBe('https://example.com/hook'); (0, vitest_1.expect)(endpoint.enabled).toBe(true); (0, vitest_1.expect)(endpoint.secret.value).toBeTruthy(); (0, vitest_1.expect)(endpoint.secret.value.length).toBeGreaterThan(20); }); (0, vitest_1.it)('records delivery', () => { const endpoint = WebhookEndpoint_1.WebhookEndpoint.create({ url: 'https://example.com/hook' }); (0, vitest_1.expect)(endpoint.lastStatus).toBeUndefined(); endpoint.recordDelivery(200); (0, vitest_1.expect)(endpoint.lastStatus).toBe(200); (0, vitest_1.expect)(endpoint.lastDeliveredAt).toBeDefined(); }); }); // ─── WebhookSecret ──────────────────────────────────────────────────────────── (0, vitest_1.describe)('WebhookSecret', () => { (0, vitest_1.it)('generates a secret', () => { const s = WebhookSecret_1.WebhookSecret.generate(); (0, vitest_1.expect)(s.value.length).toBeGreaterThan(20); }); (0, vitest_1.it)('fromString round-trips', () => { const s = WebhookSecret_1.WebhookSecret.fromString('mysecret-at-least-16chars'); (0, vitest_1.expect)(s.value).toBe('mysecret-at-least-16chars'); }); (0, vitest_1.it)('throws when secret too short', () => { (0, vitest_1.expect)(() => WebhookSecret_1.WebhookSecret.fromString('short')).toThrow(); }); }); // ─── HMAC signature verification ───────────────────────────────────────────── (0, vitest_1.describe)('HMAC webhook signature', () => { (0, vitest_1.it)('produces valid sha256 signature', () => { const secret = 'test-secret-abc123'; const body = JSON.stringify({ event: 'finding.created', data: { id: '1' } }); const sig = (0, crypto_1.createHmac)('sha256', secret).update(body).digest('hex'); (0, vitest_1.expect)(sig).toBeTruthy(); // Verify it's a valid hex string of 64 chars (sha256) (0, vitest_1.expect)(sig).toMatch(/^[0-9a-f]{64}$/); }); (0, vitest_1.it)('same body + secret → same signature', () => { const secret = 'test-secret'; const body = 'hello world'; const sig1 = (0, crypto_1.createHmac)('sha256', secret).update(body).digest('hex'); const sig2 = (0, crypto_1.createHmac)('sha256', secret).update(body).digest('hex'); (0, vitest_1.expect)(sig1).toBe(sig2); }); (0, vitest_1.it)('different body → different signature', () => { const secret = 'test-secret'; const sig1 = (0, crypto_1.createHmac)('sha256', secret).update('body1').digest('hex'); const sig2 = (0, crypto_1.createHmac)('sha256', secret).update('body2').digest('hex'); (0, vitest_1.expect)(sig1).not.toBe(sig2); }); }); // ─── WebhookDispatcher ─────────────────────────────────────────────────────── (0, vitest_1.describe)('WebhookDispatcher', () => { const logger = (0, pino_1.pino)({ level: 'silent' }); (0, vitest_1.it)('calls fetch for each enabled endpoint', async () => { const secret = WebhookSecret_1.WebhookSecret.fromString('secret123456789abcdef'); const endpoint = WebhookEndpoint_1.WebhookEndpoint.reconstitute({ url: 'https://example.com/hook', secret, enabled: true, createdAt: new Date() }, { toString: () => 'ep-1', equals: () => false }); const mockRepo = { save: vitest_1.vi.fn(), findById: vitest_1.vi.fn(), findAll: vitest_1.vi.fn(), findEnabled: vitest_1.vi.fn().mockResolvedValue([endpoint]), update: vitest_1.vi.fn(), delete: vitest_1.vi.fn(), }; const fetchMock = vitest_1.vi.fn().mockResolvedValue({ status: 200, ok: true }); global.fetch = fetchMock; const dispatcher = new WebhookDispatcher_1.WebhookDispatcher(mockRepo, logger); const finding = { id: 'f-1', title: 'XSS in login form', severity: 'high', type: 'xss', description: 'Reflected XSS', sessionId: 's-1', }; await dispatcher.dispatchFinding(finding); (0, vitest_1.expect)(fetchMock).toHaveBeenCalledOnce(); const [url, opts] = fetchMock.mock.calls[0]; (0, vitest_1.expect)(url).toBe('https://example.com/hook'); (0, vitest_1.expect)(opts.method).toBe('POST'); const headers = opts.headers; (0, vitest_1.expect)(headers['X-ABE-Event']).toBe('finding.created'); (0, vitest_1.expect)(headers['X-ABE-Signature']).toMatch(/^sha256=[0-9a-f]{64}$/); }); (0, vitest_1.it)('does not throw when no endpoints', async () => { const mockRepo = { save: vitest_1.vi.fn(), findById: vitest_1.vi.fn(), findAll: vitest_1.vi.fn(), findEnabled: vitest_1.vi.fn().mockResolvedValue([]), update: vitest_1.vi.fn(), delete: vitest_1.vi.fn(), }; const dispatcher = new WebhookDispatcher_1.WebhookDispatcher(mockRepo, logger); const finding = { id: 'f-1', title: 'Test', severity: 'low', type: 'info', description: 'Test', sessionId: 's-1', }; await (0, vitest_1.expect)(dispatcher.dispatchFinding(finding)).resolves.toBeUndefined(); }); });