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'); }); }); });