import { describe, it, expect } from 'vitest'; import { SseConnectionIndex, type IndexedSSEConnection } from '../server/utils/sse-connection-index'; function conn(id: string, userId: number, organizationId: number): IndexedSSEConnection { return { id, userId, organizationId }; } describe('SseConnectionIndex', () => { it('add/remove ведут учёт размера и очищают оба индекса', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); index.add(conn('b', 1, 10)); index.add(conn('c', 2, 20)); expect(index.size).toBe(3); index.remove('a'); expect(index.size).toBe(2); // Соединение удалено из всех индексов — не придёт ни по org, ни по user expect(Array.from(index.getForEventTarget(10)).map(c => c.id)).toEqual(['b']); expect(Array.from(index.getForEventTarget(10, 1)).map(c => c.id)).toEqual(['b']); index.remove('b'); index.remove('c'); expect(index.size).toBe(0); // Пустые Set'ы убраны из карт expect(Array.from(index.getForEventTarget(10))).toEqual([]); expect(Array.from(index.getForEventTarget(20, 2))).toEqual([]); }); it('remove несуществующего id — no-op', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); expect(index.remove('missing')).toBeUndefined(); expect(index.size).toBe(1); }); it('событие организации уходит только соединениям этой организации', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); index.add(conn('b', 2, 10)); index.add(conn('c', 3, 20)); const targets = Array.from(index.getForEventTarget(10)).map(c => c.id); expect(targets).toEqual(['a', 'b']); }); it('событие пользователя без organizationId уходит всем его соединениям (любая org)', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); index.add(conn('b', 1, 20)); index.add(conn('c', 2, 10)); const targets = Array.from(index.getForEventTarget(undefined, 1)).map(c => c.id); expect(targets).toEqual(['a', 'b']); }); it('событие пользователя + organizationId уходит только его соединениям в этой org', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); index.add(conn('b', 1, 20)); index.add(conn('c', 2, 10)); const targets = Array.from(index.getForEventTarget(10, 1)).map(c => c.id); expect(targets).toEqual(['a']); }); it('событие без userId и organizationId — broadcast на все соединения', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); index.add(conn('b', 2, 20)); const targets = Array.from(index.getForEventTarget(undefined, undefined)).map(c => c.id); expect(targets).toEqual(['a', 'b']); }); it('isUserConnected учитывает и пользователя, и организацию', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); expect(index.isUserConnected(1, 10)).toBe(true); expect(index.isUserConnected(1, 20)).toBe(false); expect(index.isUserConnected(2, 10)).toBe(false); index.remove('a'); expect(index.isUserConnected(1, 10)).toBe(false); }); it('forEach обходит все соединения (глобальный heartbeat)', () => { const index = new SseConnectionIndex(); index.add(conn('a', 1, 10)); index.add(conn('b', 2, 20)); const seen: string[] = []; index.forEach(c => seen.push(c.id)); expect(seen).toEqual(['a', 'b']); }); });