Scaffold dd0c/alert: ingestion, correlation engine, HMAC validation, tests
- Webhook ingestion: HMAC validation for Datadog/PagerDuty/OpsGenie with 5-min timestamp freshness - Payload normalizers: canonical alert schema with severity mapping per provider - Correlation engine: time-window grouping, late-alert attachment (2x window), FakeClock for testing - InMemoryWindowStore for unit tests - Tests: 12 HMAC validation cases, 5 normalizer cases, 7 correlation engine cases - PostgreSQL schema with RLS: tenants, incidents, alerts, webhook_secrets, notification_configs - Free tier enforcement columns (alert_count_month, reset_at) - Fly.io config, Dockerfile, Gitea Actions CI
This commit is contained in:
138
products/03-alert-intelligence/tests/unit/ingestion.test.ts
Normal file
138
products/03-alert-intelligence/tests/unit/ingestion.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
validateDatadogHmac,
|
||||
validatePagerdutyHmac,
|
||||
validateOpsgenieHmac,
|
||||
normalizeDatadog,
|
||||
normalizePagerduty,
|
||||
normalizeOpsgenie,
|
||||
} from '../../src/ingestion/webhook.js';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const SECRET = 'test-webhook-secret';
|
||||
|
||||
describe('HMAC Validation', () => {
|
||||
describe('Datadog', () => {
|
||||
it('accepts valid signature with fresh timestamp', () => {
|
||||
const body = '{"alert_id":"123"}';
|
||||
const ts = Math.floor(Date.now() / 1000).toString();
|
||||
const sig = crypto.createHmac('sha256', SECRET).update(ts + body).digest('hex');
|
||||
|
||||
const result = validateDatadogHmac(body, sig, ts, SECRET);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects stale timestamp (>5 minutes)', () => {
|
||||
const body = '{"alert_id":"123"}';
|
||||
const ts = (Math.floor(Date.now() / 1000) - 301).toString();
|
||||
const sig = crypto.createHmac('sha256', SECRET).update(ts + body).digest('hex');
|
||||
|
||||
const result = validateDatadogHmac(body, sig, ts, SECRET);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('stale timestamp');
|
||||
});
|
||||
|
||||
it('rejects missing signature', () => {
|
||||
const result = validateDatadogHmac('{}', undefined, '123', SECRET);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid signature', () => {
|
||||
const ts = Math.floor(Date.now() / 1000).toString();
|
||||
const result = validateDatadogHmac('{}', 'bad-sig', ts, SECRET);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PagerDuty', () => {
|
||||
it('rejects missing signature', () => {
|
||||
const result = validatePagerdutyHmac('{}', undefined, SECRET);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects malformed signature', () => {
|
||||
const result = validatePagerdutyHmac('{}', 'garbage', SECRET);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpsGenie', () => {
|
||||
it('rejects stale timestamp from payload body', () => {
|
||||
const staleTs = Date.now() - 6 * 60 * 1000; // 6 minutes ago
|
||||
const body = JSON.stringify({ alert: { alertId: '1' }, timestamp: staleTs });
|
||||
const sig = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
|
||||
|
||||
const result = validateOpsgenieHmac(body, sig, SECRET);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.error).toContain('stale timestamp');
|
||||
});
|
||||
|
||||
it('accepts fresh payload', () => {
|
||||
const body = JSON.stringify({ alert: { alertId: '1' }, timestamp: Date.now() });
|
||||
const sig = crypto.createHmac('sha256', SECRET).update(body).digest('hex');
|
||||
|
||||
const result = validateOpsgenieHmac(body, sig, SECRET);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Payload Normalizers', () => {
|
||||
it('normalizes Datadog payload', () => {
|
||||
const alert = normalizeDatadog({
|
||||
alert_id: 'dd-123',
|
||||
title: 'High CPU',
|
||||
priority: 'P1',
|
||||
alert_transition: 'Triggered',
|
||||
tags: { service: 'auth', env: 'prod' },
|
||||
date_happened: 1709251200,
|
||||
});
|
||||
expect(alert.sourceProvider).toBe('datadog');
|
||||
expect(alert.severity).toBe('critical');
|
||||
expect(alert.status).toBe('firing');
|
||||
expect(alert.service).toBe('auth');
|
||||
});
|
||||
|
||||
it('normalizes PagerDuty payload', () => {
|
||||
const alert = normalizePagerduty({
|
||||
event: {
|
||||
event_type: 'incident.triggered',
|
||||
data: {
|
||||
id: 'pd-456',
|
||||
title: 'Disk Full',
|
||||
urgency: 'high',
|
||||
service: { name: 'storage' },
|
||||
created_at: '2026-03-01T00:00:00Z',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(alert.sourceProvider).toBe('pagerduty');
|
||||
expect(alert.severity).toBe('critical');
|
||||
expect(alert.service).toBe('storage');
|
||||
});
|
||||
|
||||
it('normalizes OpsGenie payload', () => {
|
||||
const alert = normalizeOpsgenie({
|
||||
action: 'Create',
|
||||
alert: {
|
||||
alertId: 'og-789',
|
||||
message: 'Memory Leak',
|
||||
priority: 'P2',
|
||||
tags: ['service:api'],
|
||||
},
|
||||
});
|
||||
expect(alert.sourceProvider).toBe('opsgenie');
|
||||
expect(alert.severity).toBe('high');
|
||||
expect(alert.status).toBe('firing');
|
||||
});
|
||||
|
||||
it('maps Datadog Recovered to resolved', () => {
|
||||
const alert = normalizeDatadog({ alert_transition: 'Recovered' });
|
||||
expect(alert.status).toBe('resolved');
|
||||
});
|
||||
|
||||
it('maps OpsGenie Close to resolved', () => {
|
||||
const alert = normalizeOpsgenie({ action: 'Close', alert: {} });
|
||||
expect(alert.status).toBe('resolved');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user