fase(17): licensing module with RSA validation
This commit is contained in:
167
dist/modules/integrations/__tests__/integrations.test.js
vendored
Normal file
167
dist/modules/integrations/__tests__/integrations.test.js
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
"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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user