251 lines
6.8 KiB
TypeScript
251 lines
6.8 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { EventBus } from './memory.js';
|
|
import type { PlatformEvent } from './types.js';
|
|
|
|
describe('EventBus', () => {
|
|
let bus: EventBus;
|
|
|
|
beforeEach(() => {
|
|
bus = new EventBus();
|
|
});
|
|
|
|
describe('on / emit', () => {
|
|
it('should deliver events to registered handlers', async () => {
|
|
const handler = vi.fn();
|
|
bus.on('user.created', handler);
|
|
|
|
await bus.emit('user.created', {
|
|
userId: 'u1',
|
|
email: 'test@example.com',
|
|
plan: 'free',
|
|
productId: 'lysnrai',
|
|
});
|
|
|
|
expect(handler).toHaveBeenCalledOnce();
|
|
expect(handler.mock.calls[0][0]).toMatchObject({
|
|
type: 'user.created',
|
|
payload: { userId: 'u1', email: 'test@example.com' },
|
|
});
|
|
});
|
|
|
|
it('should include event id and timestamp', async () => {
|
|
let received: PlatformEvent<'user.created'> | undefined;
|
|
bus.on('user.created', e => {
|
|
received = e;
|
|
});
|
|
|
|
await bus.emit('user.created', {
|
|
userId: 'u1',
|
|
email: 'a@b.com',
|
|
plan: 'free',
|
|
productId: 'test',
|
|
});
|
|
|
|
expect(received).toBeDefined();
|
|
expect(received!.id).toBeTruthy();
|
|
expect(received!.timestamp).toBeTruthy();
|
|
});
|
|
|
|
it('should deliver to multiple handlers', async () => {
|
|
const h1 = vi.fn();
|
|
const h2 = vi.fn();
|
|
const h3 = vi.fn();
|
|
bus.on('user.created', h1);
|
|
bus.on('user.created', h2);
|
|
bus.on('user.created', h3);
|
|
|
|
await bus.emit('user.created', {
|
|
userId: 'u1',
|
|
email: 'a@b.com',
|
|
plan: 'free',
|
|
productId: 'test',
|
|
});
|
|
|
|
expect(h1).toHaveBeenCalledOnce();
|
|
expect(h2).toHaveBeenCalledOnce();
|
|
expect(h3).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('should not deliver to handlers of different event types', async () => {
|
|
const userHandler = vi.fn();
|
|
const paymentHandler = vi.fn();
|
|
bus.on('user.created', userHandler);
|
|
bus.on('payment.succeeded', paymentHandler);
|
|
|
|
await bus.emit('user.created', {
|
|
userId: 'u1',
|
|
email: 'a@b.com',
|
|
plan: 'free',
|
|
productId: 'test',
|
|
});
|
|
|
|
expect(userHandler).toHaveBeenCalledOnce();
|
|
expect(paymentHandler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return result with zero handlers when no subscribers', async () => {
|
|
const result = await bus.emit('user.deleted', {
|
|
userId: 'u1',
|
|
productId: 'test',
|
|
});
|
|
|
|
expect(result.handlerCount).toBe(0);
|
|
expect(result.errors).toHaveLength(0);
|
|
expect(result.eventId).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('error isolation', () => {
|
|
it('should not block other handlers when one throws', async () => {
|
|
const h1 = vi.fn();
|
|
const h2 = vi.fn(() => {
|
|
throw new Error('handler crash');
|
|
});
|
|
const h3 = vi.fn();
|
|
|
|
bus.on('user.created', h1);
|
|
bus.on('user.created', h2);
|
|
bus.on('user.created', h3);
|
|
|
|
const result = await bus.emit('user.created', {
|
|
userId: 'u1',
|
|
email: 'a@b.com',
|
|
plan: 'free',
|
|
productId: 'test',
|
|
});
|
|
|
|
expect(h1).toHaveBeenCalledOnce();
|
|
expect(h2).toHaveBeenCalledOnce();
|
|
expect(h3).toHaveBeenCalledOnce();
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0].error).toBe('handler crash');
|
|
});
|
|
|
|
it('should handle async handler rejection', async () => {
|
|
bus.on('payment.failed', async () => {
|
|
throw new Error('async fail');
|
|
});
|
|
|
|
const result = await bus.emit('payment.failed', {
|
|
invoiceId: 'inv_1',
|
|
userId: 'u1',
|
|
amount: 999,
|
|
retryCount: 1,
|
|
productId: 'test',
|
|
});
|
|
|
|
expect(result.errors).toHaveLength(1);
|
|
expect(result.errors[0].error).toBe('async fail');
|
|
});
|
|
});
|
|
|
|
describe('unsubscribe', () => {
|
|
it('should stop delivering events after unsubscribe', async () => {
|
|
const handler = vi.fn();
|
|
const sub = bus.on('user.created', handler);
|
|
|
|
await bus.emit('user.created', {
|
|
userId: 'u1',
|
|
email: 'a@b.com',
|
|
plan: 'free',
|
|
productId: 'test',
|
|
});
|
|
expect(handler).toHaveBeenCalledOnce();
|
|
|
|
sub.unsubscribe();
|
|
|
|
await bus.emit('user.created', {
|
|
userId: 'u2',
|
|
email: 'b@c.com',
|
|
plan: 'pro',
|
|
productId: 'test',
|
|
});
|
|
expect(handler).toHaveBeenCalledOnce(); // still 1, not 2
|
|
});
|
|
|
|
it('should return subscription metadata', () => {
|
|
const sub = bus.on('flag.toggled', () => {});
|
|
expect(sub.id).toBeTruthy();
|
|
expect(sub.eventType).toBe('flag.toggled');
|
|
expect(typeof sub.unsubscribe).toBe('function');
|
|
});
|
|
});
|
|
|
|
describe('clear', () => {
|
|
it('should remove all handlers for a specific event type', async () => {
|
|
const h1 = vi.fn();
|
|
const h2 = vi.fn();
|
|
bus.on('user.created', h1);
|
|
bus.on('payment.succeeded', h2);
|
|
|
|
bus.clear('user.created');
|
|
|
|
await bus.emit('user.created', {
|
|
userId: 'u1',
|
|
email: 'a@b.com',
|
|
plan: 'free',
|
|
productId: 'test',
|
|
});
|
|
await bus.emit('payment.succeeded', {
|
|
invoiceId: 'inv_1',
|
|
userId: 'u1',
|
|
amount: 100,
|
|
currency: 'usd',
|
|
productId: 'test',
|
|
});
|
|
|
|
expect(h1).not.toHaveBeenCalled();
|
|
expect(h2).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('should remove all handlers when called without args', () => {
|
|
bus.on('user.created', () => {});
|
|
bus.on('payment.failed', () => {});
|
|
bus.on('flag.toggled', () => {});
|
|
|
|
expect(bus.eventTypes().length).toBe(3);
|
|
bus.clear();
|
|
expect(bus.eventTypes().length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('listenerCount / eventTypes', () => {
|
|
it('should count handlers per event type', () => {
|
|
bus.on('user.created', () => {});
|
|
bus.on('user.created', () => {});
|
|
bus.on('payment.failed', () => {});
|
|
|
|
expect(bus.listenerCount('user.created')).toBe(2);
|
|
expect(bus.listenerCount('payment.failed')).toBe(1);
|
|
expect(bus.listenerCount('user.deleted')).toBe(0);
|
|
});
|
|
|
|
it('should list event types with handlers', () => {
|
|
bus.on('user.created', () => {});
|
|
bus.on('payment.failed', () => {});
|
|
|
|
const types = bus.eventTypes();
|
|
expect(types).toContain('user.created');
|
|
expect(types).toContain('payment.failed');
|
|
expect(types).not.toContain('user.deleted');
|
|
});
|
|
});
|
|
|
|
describe('source option', () => {
|
|
it('should pass source through to handlers', async () => {
|
|
let received: PlatformEvent<'user.created'> | undefined;
|
|
bus.on('user.created', e => {
|
|
received = e;
|
|
});
|
|
|
|
await bus.emit(
|
|
'user.created',
|
|
{ userId: 'u1', email: 'a@b.com', plan: 'free', productId: 'test' },
|
|
{ source: 'auth-module' }
|
|
);
|
|
|
|
expect(received?.source).toBe('auth-module');
|
|
});
|
|
});
|
|
});
|