Files
smartipc/test/test.ts

296 lines
8.0 KiB
TypeScript

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<boolean>();
const client2Received = smartpromise.defer<boolean>();
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();