import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartipc from '../ts/index.js'; import * as smartdelay from '@push.rocks/smartdelay'; import * as smartpromise from '@push.rocks/smartpromise'; import * as path from 'path'; import * as os from 'os'; const testSocketPath = path.join(os.tmpdir(), `test-smartipc-${Date.now()}.sock`); let server: smartipc.IpcServer; let client1: smartipc.IpcClient; let client2: smartipc.IpcClient; // Test basic server creation and startup tap.test('should create and start an IPC server', async () => { server = smartipc.SmartIpc.createServer({ id: 'test-server', socketPath: testSocketPath, autoCleanupSocketFile: true, heartbeat: true, heartbeatInterval: 2000 }); await server.start({ readyWhen: 'accepting' }); expect(server.getStats().isRunning).toBeTrue(); }); // Test client connection tap.test('should create and connect a client', async () => { client1 = smartipc.SmartIpc.createClient({ id: 'test-server', socketPath: testSocketPath, clientId: 'client-1', metadata: { name: 'Test Client 1' }, autoReconnect: true, heartbeat: true, clientOnly: true }); await client1.connect(); expect(client1.getIsConnected()).toBeTrue(); expect(client1.getClientId()).toEqual('client-1'); }); // Test message sending tap.test('should send messages between server and client', async () => { const messageReceived = smartpromise.defer(); // Server listens for messages server.onMessage('test-message', (payload, clientId) => { expect(payload).toEqual({ data: 'Hello Server' }); expect(clientId).toEqual('client-1'); messageReceived.resolve(); }); // Client sends message await client1.sendMessage('test-message', { data: 'Hello Server' }); await messageReceived.promise; }); // Test request/response pattern tap.test('should handle request/response pattern', async () => { // Server handles requests server.onMessage('calculate', async (payload, clientId) => { expect(payload).toHaveProperty('a'); expect(payload).toHaveProperty('b'); return { result: payload.a + payload.b }; }); // Client makes request const response = await client1.request<{a: number, b: number}, {result: number}>( 'calculate', { a: 5, b: 3 }, { timeout: 5000 } ); expect(response.result).toEqual(8); }); // Test multiple clients tap.test('should handle multiple clients', async () => { client2 = smartipc.SmartIpc.createClient({ id: 'test-server', socketPath: testSocketPath, clientId: 'client-2', metadata: { name: 'Test Client 2' }, clientOnly: true }); await client2.connect(); expect(client2.getIsConnected()).toBeTrue(); const clientIds = server.getClientIds(); expect(clientIds).toContain('client-1'); expect(clientIds).toContain('client-2'); expect(clientIds.length).toEqual(2); }); // Test broadcasting tap.test('should broadcast messages to all clients', async () => { const client1Received = smartpromise.defer(); const client2Received = smartpromise.defer(); client1.onMessage('broadcast-test', (payload) => { expect(payload).toEqual({ announcement: 'Hello everyone!' }); client1Received.resolve(); }); client2.onMessage('broadcast-test', (payload) => { expect(payload).toEqual({ announcement: 'Hello everyone!' }); client2Received.resolve(); }); await server.broadcast('broadcast-test', { announcement: 'Hello everyone!' }); await Promise.all([client1Received.promise, client2Received.promise]); }); // Test selective broadcasting tap.test('should broadcast to specific clients based on filter', async () => { const client1Received = smartpromise.defer(); const client2Received = smartpromise.defer(); client1.onMessage('selective-broadcast', () => { client1Received.resolve(true); }); client2.onMessage('selective-broadcast', () => { client2Received.resolve(true); }); // Only broadcast to client-1 await server.broadcastTo( (clientId) => clientId === 'client-1', 'selective-broadcast', { data: 'Only for client-1' } ); // Wait a bit to ensure client2 doesn't receive it await smartdelay.delayFor(500); expect(await Promise.race([ client1Received.promise, smartdelay.delayFor(100).then(() => false) ])).toBeTrue(); expect(await Promise.race([ client2Received.promise, smartdelay.delayFor(100).then(() => false) ])).toBeFalse(); }); // Test pub/sub pattern tap.test('should handle pub/sub pattern', async () => { const messageReceived = smartpromise.defer(); // Client 1 subscribes to a topic await client1.subscribe('news', (payload) => { expect(payload).toEqual({ headline: 'Breaking news!' }); messageReceived.resolve(); }); // Client 2 publishes to the topic await client2.publish('news', { headline: 'Breaking news!' }); await messageReceived.promise; }); // Test error handling tap.test('should handle errors gracefully', async () => { const errorReceived = smartpromise.defer(); server.on('error', (error, clientId) => { errorReceived.resolve(); }); // Try to send to non-existent client try { await server.sendToClient('non-existent', 'test', {}); } catch (error) { expect(error.message).toContain('not found'); } }); // Test client disconnection tap.test('should handle client disconnection', async () => { const disconnectReceived = smartpromise.defer(); server.on('clientDisconnect', (clientId) => { if (clientId === 'client-2') { disconnectReceived.resolve(); } }); await client2.disconnect(); expect(client2.getIsConnected()).toBeFalse(); await disconnectReceived.promise; // Check that client is removed from server const clientIds = server.getClientIds(); expect(clientIds).toContain('client-1'); expect(clientIds).not.toContain('client-2'); }); // Test auto-reconnection tap.test('should auto-reconnect on connection loss', async () => { // This test simulates connection loss by stopping and restarting the server const reconnected = smartpromise.defer(); client1.on('reconnecting', (info) => { expect(info).toHaveProperty('attempt'); expect(info).toHaveProperty('delay'); }); client1.on('connect', () => { reconnected.resolve(); }); // Stop the server to simulate connection loss await server.stop(); // Wait a bit await smartdelay.delayFor(500); // Restart the server await server.start(); // Wait for client to reconnect await reconnected.promise; expect(client1.getIsConnected()).toBeTrue(); }); // Test TCP transport tap.test('should work with TCP transport', async () => { const tcpServer = smartipc.SmartIpc.createServer({ id: 'tcp-test-server', host: 'localhost', port: 8765, heartbeat: false }); await tcpServer.start(); const tcpClient = smartipc.SmartIpc.createClient({ id: 'tcp-test-server', host: 'localhost', port: 8765, clientId: 'tcp-client-1' }); await tcpClient.connect(); expect(tcpClient.getIsConnected()).toBeTrue(); // Test message exchange const messageReceived = smartpromise.defer(); tcpServer.onMessage('tcp-test', (payload, clientId) => { expect(payload).toEqual({ data: 'TCP works!' }); messageReceived.resolve(); }); await tcpClient.sendMessage('tcp-test', { data: 'TCP works!' }); await messageReceived.promise; await tcpClient.disconnect(); await tcpServer.stop(); }); // Test message timeout tap.test('should timeout requests when no response is received', async () => { // Don't register a handler for this message type try { await client1.request( 'non-existent-handler', { data: 'test' }, { timeout: 1000 } ); expect(true).toBeFalse(); // Should not reach here } catch (error) { expect(error.message).toContain('timeout'); } }); // Cleanup tap.test('should cleanup and close all connections', async () => { await client1.disconnect(); await server.stop(); expect(server.getStats().isRunning).toBeFalse(); expect(client1.getIsConnected()).toBeFalse(); }); export default tap.start();