test(backend): verify scheduler webhook lifecycle
This commit is contained in:
parent
8f7247c413
commit
3aa385774e
79
backend/src/lib/webhook-subscriber.test.ts
Normal file
79
backend/src/lib/webhook-subscriber.test.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { dispatchToTargetsMock } = vi.hoisted(() => ({
|
||||
dispatchToTargetsMock: vi.fn(async () => []),
|
||||
}));
|
||||
|
||||
vi.mock('@bytelyst/webhook-dispatch', () => ({
|
||||
dispatchToTargets: dispatchToTargetsMock,
|
||||
}));
|
||||
|
||||
import { _resetEventBus, getEventBus } from './event-bus.js';
|
||||
import {
|
||||
_resetWebhookSubscriberForTests,
|
||||
initWebhookSubscriber,
|
||||
isWebhookSubscriberRunning,
|
||||
registerWebhookTarget,
|
||||
stopWebhookSubscriber,
|
||||
} from './webhook-subscriber.js';
|
||||
|
||||
describe('webhook subscriber lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
_resetWebhookSubscriberForTests();
|
||||
_resetEventBus();
|
||||
dispatchToTargetsMock.mockClear();
|
||||
});
|
||||
|
||||
it('does not subscribe until initialized', async () => {
|
||||
registerWebhookTarget({
|
||||
id: 'target-1',
|
||||
url: 'https://example.com/webhook',
|
||||
secret: 'secret',
|
||||
events: ['note.created'],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
await getEventBus().emit('note.created', {
|
||||
noteId: 'note-1',
|
||||
workspaceId: 'ws-1',
|
||||
userId: 'user-1',
|
||||
title: 'Created',
|
||||
});
|
||||
|
||||
expect(isWebhookSubscriberRunning()).toBe(false);
|
||||
expect(dispatchToTargetsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('initializes idempotently and unsubscribes on stop', async () => {
|
||||
registerWebhookTarget({
|
||||
id: 'target-1',
|
||||
url: 'https://example.com/webhook',
|
||||
secret: 'secret',
|
||||
events: ['note.created'],
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
initWebhookSubscriber();
|
||||
initWebhookSubscriber();
|
||||
expect(isWebhookSubscriberRunning()).toBe(true);
|
||||
|
||||
await getEventBus().emit('note.created', {
|
||||
noteId: 'note-1',
|
||||
workspaceId: 'ws-1',
|
||||
userId: 'user-1',
|
||||
title: 'Created',
|
||||
});
|
||||
expect(dispatchToTargetsMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
stopWebhookSubscriber();
|
||||
expect(isWebhookSubscriberRunning()).toBe(false);
|
||||
|
||||
await getEventBus().emit('note.created', {
|
||||
noteId: 'note-2',
|
||||
workspaceId: 'ws-1',
|
||||
userId: 'user-1',
|
||||
title: 'Created again',
|
||||
});
|
||||
expect(dispatchToTargetsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -73,6 +73,8 @@ async function dispatchEvent<K extends keyof NoteLettEventMap>(
|
||||
const unsubscribers: (() => void)[] = [];
|
||||
|
||||
export function initWebhookSubscriber(): void {
|
||||
if (unsubscribers.length > 0) return;
|
||||
|
||||
const bus = getEventBus();
|
||||
|
||||
unsubscribers.push(
|
||||
@ -84,7 +86,18 @@ export function initWebhookSubscriber(): void {
|
||||
);
|
||||
}
|
||||
|
||||
export function isWebhookSubscriberRunning(): boolean {
|
||||
return unsubscribers.length > 0;
|
||||
}
|
||||
|
||||
export function stopWebhookSubscriber(): void {
|
||||
for (const unsub of unsubscribers) unsub();
|
||||
unsubscribers.length = 0;
|
||||
}
|
||||
|
||||
/** @internal — for testing only. */
|
||||
export function _resetWebhookSubscriberForTests(): void {
|
||||
stopWebhookSubscriber();
|
||||
targets.length = 0;
|
||||
deliveryLog = [];
|
||||
}
|
||||
|
||||
@ -59,7 +59,12 @@ vi.mock('@bytelyst/llm', () => ({
|
||||
}));
|
||||
|
||||
import { buildTestApp, resetMemoryDatastore } from '../../test-helpers.js';
|
||||
import { promptSchedulerRoutes } from './scheduler.js';
|
||||
import {
|
||||
isSchedulerLoopRunning,
|
||||
promptSchedulerRoutes,
|
||||
startSchedulerLoop,
|
||||
stopSchedulerLoop,
|
||||
} from './scheduler.js';
|
||||
import { notePromptRoutes } from './routes.js';
|
||||
import { noteRoutes } from '../notes/routes.js';
|
||||
import { upsertBuiltinTemplate } from './repository.js';
|
||||
@ -76,6 +81,7 @@ beforeAll(async () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
stopSchedulerLoop();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
@ -410,3 +416,24 @@ describe('scheduler diagnostics', () => {
|
||||
expect(Array.isArray(body.nextRuns)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scheduler lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
stopSchedulerLoop();
|
||||
});
|
||||
|
||||
it('does not start the scheduler loop when only routes are registered for tests', () => {
|
||||
expect(isSchedulerLoopRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('starts idempotently and stops cleanly', () => {
|
||||
startSchedulerLoop(60_000);
|
||||
expect(isSchedulerLoopRunning()).toBe(true);
|
||||
|
||||
startSchedulerLoop(60_000);
|
||||
expect(isSchedulerLoopRunning()).toBe(true);
|
||||
|
||||
stopSchedulerLoop();
|
||||
expect(isSchedulerLoopRunning()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -243,6 +243,10 @@ export function startSchedulerLoop(intervalMs = 60_000, logger?: SchedulerLogger
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
export function isSchedulerLoopRunning(): boolean {
|
||||
return schedulerInterval !== null;
|
||||
}
|
||||
|
||||
export function stopSchedulerLoop(): void {
|
||||
if (schedulerInterval) {
|
||||
clearInterval(schedulerInterval);
|
||||
|
||||
@ -8,6 +8,8 @@ const initDatastoreMock = vi.fn(() => undefined);
|
||||
const diagnosticsRoutesMock = vi.fn(async () => undefined);
|
||||
const startSchedulerLoopMock = vi.fn();
|
||||
const stopSchedulerLoopMock = vi.fn();
|
||||
const initWebhookSubscriberMock = vi.fn();
|
||||
const stopWebhookSubscriberMock = vi.fn();
|
||||
|
||||
const appMock = {
|
||||
register: vi.fn(async () => undefined),
|
||||
@ -74,6 +76,10 @@ vi.mock('./lib/request-context.js', () => ({
|
||||
vi.mock('./lib/feature-flags.js', () => ({ getAllFlags: vi.fn(() => ({})) }));
|
||||
vi.mock('./lib/telemetry.js', () => ({ getBufferedEvents: vi.fn(() => []), flushEvents: vi.fn(() => []) }));
|
||||
vi.mock('./lib/diagnostics-routes.js', () => ({ diagnosticsRoutes: diagnosticsRoutesMock }));
|
||||
vi.mock('./lib/webhook-subscriber.js', () => ({
|
||||
initWebhookSubscriber: initWebhookSubscriberMock,
|
||||
stopWebhookSubscriber: stopWebhookSubscriberMock,
|
||||
}));
|
||||
vi.mock('./modules/note-shares/repository.js', () => ({ findShareByToken: vi.fn(async () => null) }));
|
||||
vi.mock('./modules/notes/repository.js', () => ({ getNote: vi.fn(async () => null) }));
|
||||
|
||||
@ -99,6 +105,13 @@ describe('server bootstrap', () => {
|
||||
expect(appMock.register).toHaveBeenCalledTimes(14);
|
||||
expect(diagnosticsRoutesMock).toHaveBeenCalledWith(appMock);
|
||||
expect(startSchedulerLoopMock).toHaveBeenCalledWith(60_000, appMock.log);
|
||||
expect(initWebhookSubscriberMock).toHaveBeenCalledOnce();
|
||||
expect(appMock.addHook).toHaveBeenCalledWith('onClose', expect.any(Function));
|
||||
const onCloseHook = appMock.addHook.mock.calls.find(([event]) => event === 'onClose')?.[1];
|
||||
expect(onCloseHook).toEqual(expect.any(Function));
|
||||
await onCloseHook();
|
||||
expect(stopSchedulerLoopMock).toHaveBeenCalledOnce();
|
||||
expect(stopWebhookSubscriberMock).toHaveBeenCalledOnce();
|
||||
expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4016, host: '0.0.0.0' });
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user