docs: enterprise refactor plan with ralph specs
This commit is contained in:
131
tests/server/notifications.test.ts
Normal file
131
tests/server/notifications.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Unit tests for SlackNotifier, WebhookNotifier, and NotificationService.
|
||||
* HTTP calls are mocked via jest.spyOn on global fetch.
|
||||
*/
|
||||
|
||||
import { SlackNotifier } from '../../src/server/notifications/SlackNotifier';
|
||||
import { WebhookNotifier } from '../../src/server/notifications/WebhookNotifier';
|
||||
import { NotificationService } from '../../src/server/notifications/NotificationService';
|
||||
import { IAnomaly } from '../../src/core/interfaces';
|
||||
|
||||
function makeAnomaly(overrides: Partial<IAnomaly> = {}): IAnomaly {
|
||||
return {
|
||||
id: 'anom_1',
|
||||
type: 'http_error',
|
||||
severity: 'high',
|
||||
observationId: 'obs_1',
|
||||
actionTrace: [],
|
||||
description: 'Test anomaly',
|
||||
evidence: {},
|
||||
timestamp: 1000000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function mockFetch(ok: boolean, text = 'ok') {
|
||||
return jest.spyOn(global, 'fetch').mockResolvedValue({
|
||||
ok,
|
||||
status: ok ? 200 : 500,
|
||||
text: () => Promise.resolve(text),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ─── SlackNotifier ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('SlackNotifier', () => {
|
||||
it('sends a POST to the Slack webhook URL', async () => {
|
||||
const spy = mockFetch(true);
|
||||
const notifier = new SlackNotifier('https://hooks.slack.com/test');
|
||||
await notifier.send(makeAnomaly(), 'sess_1', 'http://app.com');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const [url, opts] = spy.mock.calls[0]!;
|
||||
expect(url).toBe('https://hooks.slack.com/test');
|
||||
expect((opts as RequestInit).method).toBe('POST');
|
||||
const body = JSON.parse((opts as RequestInit).body as string);
|
||||
expect(body.blocks).toBeDefined();
|
||||
});
|
||||
|
||||
it('throws when Slack returns non-200', async () => {
|
||||
mockFetch(false, 'channel_not_found');
|
||||
const notifier = new SlackNotifier('https://hooks.slack.com/test');
|
||||
await expect(notifier.send(makeAnomaly(), 'sess_1', 'http://app.com')).rejects.toThrow('500');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── WebhookNotifier ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('WebhookNotifier', () => {
|
||||
it('sends a POST with anomaly JSON to webhook URL', async () => {
|
||||
const spy = mockFetch(true);
|
||||
const notifier = new WebhookNotifier('https://myapp.com/webhooks/abe');
|
||||
await notifier.send(makeAnomaly());
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const [url, opts] = spy.mock.calls[0]!;
|
||||
expect(url).toBe('https://myapp.com/webhooks/abe');
|
||||
expect((opts as RequestInit).method).toBe('POST');
|
||||
const body = JSON.parse((opts as RequestInit).body as string);
|
||||
expect(body.id).toBe('anom_1');
|
||||
});
|
||||
|
||||
it('throws when webhook returns non-200', async () => {
|
||||
mockFetch(false, 'bad gateway');
|
||||
const notifier = new WebhookNotifier('https://myapp.com/webhooks/abe');
|
||||
await expect(notifier.send(makeAnomaly())).rejects.toThrow('500');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── NotificationService ─────────────────────────────────────────────────────
|
||||
|
||||
describe('NotificationService', () => {
|
||||
it('skips notifications below minSeverity', async () => {
|
||||
const spy = mockFetch(true);
|
||||
const service = new NotificationService({
|
||||
webhookUrl: 'https://myapp.com/hooks',
|
||||
minSeverity: 'high',
|
||||
});
|
||||
await service.notify(makeAnomaly({ severity: 'low' }), 'sess_1', 'http://app.com');
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends notification at or above minSeverity', async () => {
|
||||
const spy = mockFetch(true);
|
||||
const service = new NotificationService({
|
||||
webhookUrl: 'https://myapp.com/hooks',
|
||||
minSeverity: 'high',
|
||||
});
|
||||
await service.notify(makeAnomaly({ severity: 'high' }), 'sess_1', 'http://app.com');
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls persister with success record on successful send', async () => {
|
||||
mockFetch(true);
|
||||
const persisted: unknown[] = [];
|
||||
const service = new NotificationService({
|
||||
webhookUrl: 'https://myapp.com/hooks',
|
||||
minSeverity: 'low',
|
||||
persister: (r) => persisted.push(r),
|
||||
});
|
||||
await service.notify(makeAnomaly({ severity: 'medium' }), 'sess_1', 'http://app.com');
|
||||
expect(persisted).toHaveLength(1);
|
||||
expect((persisted[0] as { status: string }).status).toBe('success');
|
||||
});
|
||||
|
||||
it('calls persister with failed record on send failure', async () => {
|
||||
mockFetch(false, 'error');
|
||||
jest.useFakeTimers();
|
||||
const persisted: unknown[] = [];
|
||||
const service = new NotificationService({
|
||||
webhookUrl: 'https://myapp.com/hooks',
|
||||
minSeverity: 'low',
|
||||
persister: (r) => persisted.push(r),
|
||||
});
|
||||
await service.notify(makeAnomaly({ severity: 'medium' }), 'sess_1', 'http://app.com');
|
||||
expect(persisted).toHaveLength(1);
|
||||
expect((persisted[0] as { status: string }).status).toBe('failed');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user