Related
I have been trying to establish TCP hole punching with Node.js, and I am not sure if it fails because of my NAT or because the code is erroneous.
The following code intends to:
let 2 clients register on a server the 4-tuple (address on client, port on client, address as seen by server, port as seen by server)
let the server signal when the 2 clients are mutually ready by sending them each other's 4-tuple (tryConnectToPeer)
let each client start a local server (listen) on the local address and port used when communicating with the server (address on client, port on client)
when the local server is running, try to establish a connection (connect) with the local port & address of the other client, as well as an external connection with the port & address of the other client, as the server was seeing them (probably the other client's router address and port then)
Client code - I would imagine the mistake is here:
import { createConnection, createServer } from 'net';
const serverPort = 9999;
const serverHost = '192.168.1.19'
const socket = createConnection(serverPort, serverHost);
socket.setEncoding('utf8');
socket.on('data', (data: string) => {
console.log('data', data);
let parsedData: any = null;
try {
parsedData = JSON.parse(data);
} catch (e) {
if (e instanceof Error) {
console.log(e.message);
} else {
throw e;
}
}
if (parsedData?.command === 'tryConnectToPeer') {
console.log('Will try to connect with peer:', parsedData);
const server = createServer(c => {
console.log('client connected');
c.setEncoding('utf8');
c.on('data', (data: string) => {
console.log('received:', data);
c.write('hi!');
});
});
server.listen(socket.localPort, socket.localAddress, () => {
console.log('server bound to ', socket.localAddress, socket.localPort);
});
server.on('listening', () => {
console.log('Attempting local connection', parsedData.localAddress, parsedData.localPort);
const localSocket = createConnection({ port: parsedData.localPort, host: parsedData.localAddress });
localSocket.on('error', (e) => {
console.error('Failed to connect with peer locally');
console.error(e);
});
localSocket.setEncoding('utf8');
localSocket.on('data', (data: string) => {
console.log(data);
localSocket.write('ho! on local')
})
console.log('Attempting external connection', parsedData.externalAddress, parsedData.externalPort);
const externalSocket = createConnection({ port: parsedData.externalPort, host: parsedData.externalAddress});
externalSocket.on('error', (e) => {
console.error('Failed to connect with peer externally');
console.error(e);
});
externalSocket.setEncoding('utf8');
externalSocket.on('data', (data: string) => {
console.log(data);
externalSocket.write('ho! on external')
})
localSocket.on('connect', () => {
externalSocket.end();
localSocket.write('start from localsocket');
console.log('connected to peer locally!');
})
externalSocket.on('connect', () => {
// localSocket.end();
externalSocket.write('start from externalSocket');
console.log('connected to peer externally!');
})
})
}
});
socket.on('connect', () => {
socket.write(JSON.stringify(
{
command: 'register',
localPort: socket.localPort,
localAddress: socket.localAddress
}
));
});
Server code - a tad long, but probably not the problematic piece:
import { createServer, Socket } from 'net';
type AddressAndPort = {
address: string | undefined,
port: number | undefined
}
class ConnectionDescriptor {
socket: Socket;
addressAndPortOnClient: AddressAndPort;
addressAndPortSeenByServer: AddressAndPort;
constructor({ socket, addressAndPortOnClient, addressAndPortSeenByServer } : {socket: Socket, addressAndPortOnClient: AddressAndPort, addressAndPortSeenByServer: AddressAndPort}) {
this.socket = socket;
this.addressAndPortOnClient = addressAndPortOnClient;
this.addressAndPortSeenByServer = addressAndPortSeenByServer;
}
toString() {
return JSON.stringify({
addressAndPortOnClient: this.addressAndPortOnClient,
addressAndPortSeenByServer: this.addressAndPortSeenByServer
});
}
}
class ConnectionDescriptorSet {
connectionDescriptors: ConnectionDescriptor[]
constructor() {
this.connectionDescriptors = [];
}
get full() {
return this.connectionDescriptors.length === 2;
}
add(descriptor: ConnectionDescriptor) {
if (!descriptor.addressAndPortOnClient.address || !descriptor.addressAndPortOnClient.port || !descriptor.addressAndPortSeenByServer.address || !descriptor.addressAndPortSeenByServer.port) {
throw new Error(`Cannot register incomplete connection descriptor: ${JSON.stringify(descriptor)}`);
}
const index = this.connectionDescriptors.findIndex(c => c.addressAndPortSeenByServer.address === descriptor.addressAndPortSeenByServer.address && c.addressAndPortSeenByServer.port === descriptor.addressAndPortSeenByServer.port);
if (index === -1) {
console.log('Registering new client:');
console.log(descriptor.toString());
if (this.connectionDescriptors.length === 2) {
throw new Error('Only two clients can be registered at a time!');
}
this.connectionDescriptors.push(descriptor)
} else {
console.log('Client already registered:');
console.log(descriptor.toString());
}
}
remove(addressAndPortSeenByServer: AddressAndPort) {
const index = this.connectionDescriptors.findIndex(c => c.addressAndPortSeenByServer.address === addressAndPortSeenByServer.address && c.addressAndPortSeenByServer.port === addressAndPortSeenByServer.port);
if (index === -1) {
console.log('Client with following connectionDescriptors was not found for removal:');
console.log(JSON.stringify(addressAndPortSeenByServer));
} else if (index === 0) {
console.log('Removing client:');
console.log(this.connectionDescriptors[0].toString());
this.connectionDescriptors.shift();
} else if (index === 1) {
console.log('Removing client:');
console.log(this.connectionDescriptors[1].toString());
this.connectionDescriptors.pop();
} else {
throw new Error('No more than 2 clients should have been registered.');
}
}
}
const connectionDescriptorSet = new ConnectionDescriptorSet();
const server = createServer((c) => {
console.log('client connected');
// Optional - useful when logging data
c.setEncoding('utf8');
c.on('end', () => {
connectionDescriptorSet.remove({ address: c.remoteAddress, port: c.remotePort })
console.log('client disconnected');
});
c.on('data', (data: string) => {
console.log('I received:', data);
try {
const parsedData = JSON.parse(data);
if (parsedData.command === 'register') {
connectionDescriptorSet.add(new ConnectionDescriptor({
socket: c,
addressAndPortOnClient: {
address: parsedData.localAddress,
port: parsedData.localPort
},
addressAndPortSeenByServer: {
address: c.remoteAddress,
port: c.remotePort
}
}));
if (connectionDescriptorSet.full) {
console.log('connectionDescriptorSet full, broadcasting tryConnectToPeer command');
connectionDescriptorSet.connectionDescriptors[0].socket.write(
JSON.stringify({
command: 'tryConnectToPeer',
localPort: connectionDescriptorSet.connectionDescriptors[1].addressAndPortOnClient.port,
localAddress: connectionDescriptorSet.connectionDescriptors[1].addressAndPortOnClient.address,
externalAddress: connectionDescriptorSet.connectionDescriptors[1].addressAndPortSeenByServer.address,
externalPort: connectionDescriptorSet.connectionDescriptors[1].addressAndPortSeenByServer.port,
})
);
connectionDescriptorSet.connectionDescriptors[1].socket.write(
JSON.stringify({
command: 'tryConnectToPeer',
localPort: connectionDescriptorSet.connectionDescriptors[0].addressAndPortOnClient.port,
localAddress: connectionDescriptorSet.connectionDescriptors[0].addressAndPortOnClient.address,
externalAddress: connectionDescriptorSet.connectionDescriptors[0].addressAndPortSeenByServer.address,
externalPort: connectionDescriptorSet.connectionDescriptors[0].addressAndPortSeenByServer.port,
})
);
}
}
} catch (e) {
if (e instanceof Error) {
console.error(e);
c.write(e.message);
} else {
throw e;
}
}
})
});
server.on('error', (err) => {
throw err;
});
server.listen(9999, () => {
console.log('server bound');
});
This code works when the two clients are on the same local network, but fails when they are on different networks.
There is in fact no need to run a server on each client once they got each other's credentials. Having a server may increase the chances of EADDRINUSE - unsure.
A simple connect to each other suffices. First one to try will fail, second may succeed.
It's important to specify the origination port of the socket making the call to the other peer (both for the private and public calls). I believe this was the issue in the above code.
Network topology is also tricky. It's best to have the server and each client each behind a different NAT.
Working proof of concept can be found here:
https://github.com/qbalin/tcp_hole_punching_node_js/tree/main
I have redis installed on my system and its running as well.
from node application, im using below code to work with redis.
redis.js
const redis = require("redis");
let client = redis.createClient(6379, '127.0.0.1', {});
let isRedis = false;
client.on("connect", function () {
console.log(`connected to redis`);
isRedis = true;
});
client.on("error", function (err) {
console.log("redis connection error " + err);
throw err;
});
client.on("end", function (err) {
console.log("redis connection end " + err);
});
module.exports = {
SetRedis,
GetKeys,
GetRedis,
GetKeyRedis,
delRedis
};
im using node index.js command to run the application which should also give me "connected to redis" when the connection is established, but i'm not getting this message on my console .
the npm package is also present in package.json
Node Redis 4.x doesn't allow you to pass in discrete arguments for the host and port. The canonical example of connecting to Redis with Node Redis is this:
import { createClient } from 'redis';
(async () => {
const client = createClient();
client.on('error', (err) => console.log('Redis Client Error', err));
await client.connect();
await client.set('key', 'value');
const value = await client.get('key');
})();
If you want to connect to somewhere other than localhost on port 6379, I recommend using a URL. Like this:
createClient({ url: 'redis://awesome.redis.server:6380' });
But if you want finer control, you can find all the gritty configuration options in the documentation on GitHub.
I guess you are making mistake while making connection.
It should have been
let client = redis.createClient('127.0.0.1', 6379, {});
rather than
let client = redis.createClient(6379, '127.0.0.1', {});
Working redis.js,
const redis = require("redis");
let isRedis = false;
(async () => {
let client = redis.createClient(6379, '127.0.0.1', {});// create config
client.on("connect", function () {
console.log(`connected to redis`);
isRedis = true;
});
client.on("error", function (err) {
console.log("redis connection error " + err);
throw err;
});
client.on("end", function (err) {
console.log("redis connection end " + err);
});
function GetKeyRedis(key) {
return new Promise(function (resolve, reject) {
console.log("dd----",key,isRedis);
if (isRedis) {
client.get(key).then((data,err) => {
if(err){
reject(err);
}
if(data){
resolve(data)
} else {
resolve(false);
}
});
} else {
resolve(false);
}
});
}
module.exports = {
GetKeyRedis
};
await client.connect();
})();
I'm trying to set up a server to respond to socket.io clients using nodejs, express and socket.io. I want to write tests to probe the server and make sure it's handling the events correctly and sending appropriate responses to the client.
I tried writing some automated tests using jest but I couldn't figure out how to actually emit events to the server and have it respond.
Unit testing Node.js and WebSockets (Socket.io)
I checked out the above post but it didn't work for me...
Check out this boilerplate solution that's based on promises and good practice.
You can test your servers entire io events with it, no sweat.
You just need to copy a boilerplate test and add your own code as needed.
Checkout the repo on GitHub for full source code.
https://github.com/PatMan10/testing_socketIO_server
const io = require("socket.io-client");
const ev = require("../utils/events");
const logger = require("../utils/logger");
// initSocket returns a promise
// success: resolve a new socket object
// fail: reject a error
const initSocket = () => {
return new Promise((resolve, reject) => {
// create socket for communication
const socket = io("localhost:5000", {
"reconnection delay": 0,
"reopen delay": 0,
"force new connection": true
});
// define event handler for sucessfull connection
socket.on(ev.CONNECT, () => {
logger.info("connected");
resolve(socket);
});
// if connection takes longer than 5 seconds throw error
setTimeout(() => {
reject(new Error("Failed to connect wihtin 5 seconds."));
}, 5000);
});
};
// destroySocket returns a promise
// success: resolve true
// fail: reject false
const destroySocket = socket => {
return new Promise((resolve, reject) => {
// check if socket connected
if (socket.connected) {
// disconnect socket
logger.info("disconnecting...");
socket.disconnect();
resolve(true);
} else {
// not connected
logger.info("no connection to break...");
resolve(false);
}
});
};
describe("test suit: Echo & Bello", () => {
test("test: ECHO", async () => {
try {
// create socket for communication
const socketClient = await initSocket();
// create new promise for server response
const serverResponse = new Promise((resolve, reject) => {
// define a handler for the test event
socketClient.on(ev.res_ECHO, data4Client => {
//process data received from server
const { message } = data4Client;
logger.info("Server says: " + message);
// destroy socket after server responds
destroySocket(socketClient);
// return data for testing
resolve(data4Client);
});
// if response takes longer than 5 seconds throw error
setTimeout(() => {
reject(new Error("Failed to get reponse, connection timed out..."));
}, 5000);
});
// define data 4 server
const data4Server = { message: "CLIENT ECHO" };
// emit event with data to server
logger.info("Emitting ECHO event");
socketClient.emit(ev.com_ECHO, data4Server);
// wait for server to respond
const { status, message } = await serverResponse;
expect(status).toBe(200);
expect(message).toBe("SERVER ECHO");
} catch (error) {
logger.error(error);
}
});
test("test BELLO", async () => {
try {
const socketClient = await initSocket();
const serverResponse = new Promise((resolve, reject) => {
socketClient.on(ev.res_BELLO, data4Client => {
const { message } = data4Client;
logger.info("Server says: " + message);
destroySocket(socketClient);
resolve(data4Client);
});
setTimeout(() => {
reject(new Error("Failed to get reponse, connection timed out..."));
}, 5000);
});
const data4Server = { message: "CLIENT BELLO" };
logger.info("Emitting BELLO event");
socketClient.emit(ev.com_BELLO, data4Server);
const { status, message } = await serverResponse;
expect(status).toBe(200);
expect(message).toBe("SERVER BELLO");
} catch (error) {
logger.error(error);
}
});
});
I am new to node.js and would like to connect to a TCP socket. For this I am using the net module.
My idea was to wrap the connect sequence into a function then on the 'close' event, attempt a reconnection. Not that easy apparently.
function conn() {
client.connect(HOST_PORT, HOST_IP, function() {
startSequence();
})
}
client.on('close', function(e) {
log('info','Connection closed! -> ' + e)
client.destroy();
setTimeout(conn(),1000);
});
So when the remote host is closed, I see my logs comming through, howere what seems to be happening is that as soons as the remote host comes online ALL the previous attempts start to get processed - if that makes sense. If you look at client.connect, there is a function called startSequence that sends some data that "iniates" the connection from the remote server side. When the server goes offline and I start reconnecting all the failed attempts from before seem to have been buffered and are all sent together when the server goes online.
I have tried the code from this Stackoverflow link as well to no avail (Nodejs - getting client socket to try again after 5 sec time out)
client.connect(HOST_PORT, HOST_IP, function() {
pmsStartSequence();
})
// Add a 'close' event handler for the client socket
client.on('close', function(e) {
log('debug','connection closed -> ' + e)
client.setTimeout(10000, function() {
log('debug', 'trying to reconnect')
client.connect(HOST_PORT, HOST_IP, function() {
pmsStartSequence();
})
})
});
Is there any advice on how I can reconnect a socket after failure?
Inspired from the other solutions, I wrote this, it's tested, it works !
It will keep on trying every 5 sec, until connection is made, works if it looses connection too.
/* Client connection */
/* --------------------------------------------------------------------------------- */
const client = new net.Socket()
var intervalConnect = false;
function connect() {
client.connect({
port: 1338,
host: '127.0.0.1'
})
}
function launchIntervalConnect() {
if(false != intervalConnect) return
intervalConnect = setInterval(connect, 5000)
}
function clearIntervalConnect() {
if(false == intervalConnect) return
clearInterval(intervalConnect)
intervalConnect = false
}
client.on('connect', () => {
clearIntervalConnect()
logger('connected to server', 'TCP')
client.write('CLIENT connected');
})
client.on('error', (err) => {
logger(err.code, 'TCP ERROR')
launchIntervalConnect()
})
client.on('close', launchIntervalConnect)
client.on('end', launchIntervalConnect)
connect()
The problem is where you set the on-connect callback.
The doc of socket.connect() says:
connectListener ... will be added as a listener for the 'connect' event once.
By setting it in socket.connect() calls, every time you try reconnecting, one more listener (a one-time one), which calls startSequence(), is attached to that socket. Those listeners will not be fired until reconnection successes, so you got all of them triggered at the same time on a single connect.
One possible solution is separating the connect listener from socket.connect() calls.
client.on('connect', function() {
pmsStartSequence();
});
client.on('close', function(e) {
client.setTimeout(10000, function() {
client.connect(HOST_PORT, HOST_IP);
})
});
client.connect(HOST_PORT, HOST_IP);
My solution:
var parentHOST = '192.168.2.66';
var parentPORT = 9735;
var net = require('net');
var S = require('string');
var parentClient = new net.Socket();
var parentActive = false;
var startParentClient = function () {
parentClient = new net.Socket();
parentActive = false;
parentClient.connect(parentPORT, parentHOST, function() {
console.log('Connected ' + cluster.worker.id + ' to parent server: ' + parentHOST + ':' + parentPORT);
parentActive = true;
});
parentClient.on('error', function() {
parentActive = false;
console.log('Parent connection error');
});
parentClient.on('close', function() {
parentActive = false;
console.log('parent connection closed');
setTimeout(startParentClient(), 4000);
});
}
If is necessary connect:
if (!S(parentHOST).isEmpty() && !S(parentPORT).isEmpty()) {
startParentClient();
}
As mentioned multiple times in the comments, you need to use .removeAllListeners() before trying to reconnect your client to the server in order to avoid having multiple listeners on the same event.
The code below should do the trick
Note that I try to reconnect the client after the close and end events because these two events can be fired in different orders after closing a connection
const net = require("net")
let client = new net.Socket()
function connect() {
console.log("new client")
client.connect(
1337,
"127.0.0.1",
() => {
console.log("Connected")
client.write("Hello, server! Love, Client.")
}
)
client.on("data", data => {
console.log("Received: " + data)
})
client.on("close", () => {
console.log("Connection closed")
reconnect()
})
client.on("end", () => {
console.log("Connection ended")
reconnect()
})
client.on("error", console.error)
}
// function that reconnect the client to the server
reconnect = () => {
setTimeout(() => {
client.removeAllListeners() // the important line that enables you to reopen a connection
connect()
}, 1000)
}
connect()
I use the following code to achieve reconnection with node.js. I am not a Javascript expert so I guess it can be improved but it nevertheless works fine for me.
I hope this could help.
Best.
//----------------------------------------------------------------//
// SocketClient //
//----------------------------------------------------------------//
var net = require('net');
var SocketClient = function(host, port, data_handler, attempt)
{
var node_client;
var attempt_index = (attempt ? attempt : 1);
this.m_node_client = new net.Socket();
node_client = this.m_node_client;
this.m_node_client.on('close', function()
{
var new_wrapper = new SocketClient(host, port, data_handler, attempt_index + 1);
node_client.destroy();
new_wrapper.start();
});
this.m_node_client.on('data', data_handler);
this.m_node_client.on('error', function(data)
{
console.log("Error");
});
this.start = function()
{
this.m_node_client.connect(port, host, function()
{
console.log('Connected ' + attempt_index);
});
};
};
//----------------------------------------------------------------//
// Test //
//----------------------------------------------------------------//
var test_handler = function(data)
{
console.log('TestHandler[' + data + ']');
};
var wrapper = new SocketClient('127.0.0.1', 4000, test_handler);
wrapper.start();
I have tried re-using the same socket connection, by using this:
const s = net.createConnection({port});
s.once('end', () => {
s.connect({port}, () => {
});
});
that didn't work, from the server-side's perspective. If the client connection closes, it seems like a best practice to create a new connection:
const s = net.createConnection({port});
s.once('end', () => {
// create a new connection here
s = net.createConnection(...);
});
sad but true lulz.
Following this:
//
// Simple example of using net.Socket but here we capture the
// right events and attempt to re-establish the connection when
// is is closed either because of an error establishing a
// connection or when the server closes the connection.
//
// Requires
const net = require('net');
// Create socket
const port = 5555;
const host = '127.0.0.1';
const timeout = 1000;
let retrying = false;
// Functions to handle socket events
function makeConnection () {
socket.connect(port, host);
}
function connectEventHandler() {
console.log('connected');
retrying = false;
}
function dataEventHandler() {
console.log('data');
}
function endEventHandler() {
// console.log('end');
}
function timeoutEventHandler() {
// console.log('timeout');
}
function drainEventHandler() {
// console.log('drain');
}
function errorEventHandler() {
// console.log('error');
}
function closeEventHandler () {
// console.log('close');
if (!retrying) {
retrying = true;
console.log('Reconnecting...');
}
setTimeout(makeConnection, timeout);
}
// Create socket and bind callbacks
let socket = new net.Socket();
socket.on('connect', connectEventHandler);
socket.on('data', dataEventHandler);
socket.on('end', endEventHandler);
socket.on('timeout', timeoutEventHandler);
socket.on('drain', drainEventHandler);
socket.on('error', errorEventHandler);
socket.on('close', closeEventHandler);
// Connect
console.log('Connecting to ' + host + ':' + port + '...');
makeConnection();
function createServer() {
const client = new net.Socket();
client.connect(HOST_PORT, HOST_IP, function() {
console.log("Connected");
state = 1 - state;
client.write(state.toString());
});
client.on("data", function(data) {
console.log("Received: " + data);
//client.destroy(); // kill client after server's response
});
client.on("close", function() {
console.log("Connection closed");
//client.connect()
setTimeout(createServer, 2000);
});
}
createServer();
Could anyone provide a rock-solid, dead-simple unit test for Node.js using WebSockets (Socket.io)?
I'm using socket.io for Node.js, and have looked at socket.io-client for establishing the client connection to a server in the test. However, I seem to be missing something.
In the example below, "worked..." never gets printed out.
var io = require('socket.io-client')
, assert = require('assert')
, expect = require('expect.js');
describe('Suite of unit tests', function() {
describe('First (hopefully useful) test', function() {
var socket = io.connect('http://localhost:3001');
socket.on('connect', function(done) {
console.log('worked...');
done();
});
it('Doing some things with indexOf()', function() {
expect([1, 2, 3].indexOf(5)).to.be.equal(-1);
expect([1, 2, 3].indexOf(0)).to.be.equal(-1);
});
});
});
Instead, I simply get:
Suite of unit tests
First (hopefully useful) test
✓ Doing some things with indexOf()
1 test complete (26 ms)
Any suggestions?
After further poking and prodding, I found some incredibly useful information. In the author's example, he points out the critical step of establishing socket listeners in the before hooks.
This example works:
Assuming a server is listening for socket connections at localhost:3001, of course
var io = require('socket.io-client')
, assert = require('assert')
, expect = require('expect.js');
describe('Suite of unit tests', function() {
var socket;
beforeEach(function(done) {
// Setup
socket = io.connect('http://localhost:3001', {
'reconnection delay' : 0
, 'reopen delay' : 0
, 'force new connection' : true
});
socket.on('connect', function() {
console.log('worked...');
done();
});
socket.on('disconnect', function() {
console.log('disconnected...');
})
});
afterEach(function(done) {
// Cleanup
if(socket.connected) {
console.log('disconnecting...');
socket.disconnect();
} else {
// There will not be a connection unless you have done() in beforeEach, socket.on('connect'...)
console.log('no connection to break...');
}
done();
});
describe('First (hopefully useful) test', function() {
it('Doing some things with indexOf()', function(done) {
expect([1, 2, 3].indexOf(5)).to.be.equal(-1);
expect([1, 2, 3].indexOf(0)).to.be.equal(-1);
done();
});
it('Doing something else with indexOf()', function(done) {
expect([1, 2, 3].indexOf(5)).to.be.equal(-1);
expect([1, 2, 3].indexOf(0)).to.be.equal(-1);
done();
});
});
});
I found that the placement of done() in the beforeEach, socket.on('connect'...) listener was crucial to having the connection get established. For example, if you comment out done() in the listener, then add it one scope out (just before exiting the beforeEach), you'll see the "no connection to break..." message instead of the "disconnecting..." message. Like so:
beforeEach(function(done) {
// Setup
socket = io.connect('http://localhost:3001', {
'reconnection delay' : 0
, 'reopen delay' : 0
, 'force new connection' : true
});
socket.on('connect', function() {
console.log('worked...');
//done();
});
socket.on('disconnect', function() {
console.log('disconnected...');
});
done();
});
I'm new to Mocha, so there's probably a very obvious reason to the initiated for placing done() within the socket scope itself. Hopefully that little detail will save others in my shoes from hair pulling.
For me, the above test (with correct scoping of done()) outputs:
Suite of unit tests
First (hopefully useful) test
◦ Doing some things with indexOf(): worked...
✓ Doing some things with indexOf()
disconnecting...
disconnected...
◦ Doing something else with indexOf(): worked...
✓ Doing something else with indexOf()
disconnecting...
disconnected...
2 tests complete (93 ms)
Offering an extension of the accepted answer here. Has basic client to server communication useful as boilerplate for other future tests. Using mocha, chai, and expect.
var io = require('socket.io-client')
, io_server = require('socket.io').listen(3001);
describe('basic socket.io example', function() {
var socket;
beforeEach(function(done) {
// Setup
socket = io.connect('http://localhost:3001', {
'reconnection delay' : 0
, 'reopen delay' : 0
, 'force new connection' : true
, transports: ['websocket']
});
socket.on('connect', () => {
done();
});
socket.on('disconnect', () => {
// console.log('disconnected...');
});
});
afterEach((done) => {
// Cleanup
if(socket.connected) {
socket.disconnect();
}
io_server.close();
done();
});
it('should communicate', (done) => {
// once connected, emit Hello World
io_server.emit('echo', 'Hello World');
socket.once('echo', (message) => {
// Check that the message matches
expect(message).to.equal('Hello World');
done();
});
io_server.on('connection', (socket) => {
expect(socket).to.not.be.null;
});
});
});
Dealing with callbacks and promises yourself can be difficult and non trivial examples quickly become very complex and hard to read.
There is a tool called socket.io-await-test available via NPM that allows you to suspend/wait in a test until events have been triggered using the await keyword.
describe("wait for tests", () => {
it("resolves when a number of events are received", async () => {
const tester = new SocketTester(client);
const pongs = tester.on('pong');
client.emit('ping', 1);
client.emit('ping', 2);
await pongs.waitForEvents(2) // Blocks until the server emits "pong" twice.
assert.equal(pongs.get(0), 2)
assert.equal(pongs.get(1), 3)
})
})
Check out this boilerplate solution that's based on promises and good practice.
You can test your servers entire io events with it, no sweat.
You just need to copy a boilerplate test and add your own code as needed.
Checkout the repo on GitHub for full source code.
https://github.com/PatMan10/testing_socketIO_server
const io = require("socket.io-client");
const ev = require("../utils/events");
const logger = require("../utils/logger");
// initSocket returns a promise
// success: resolve a new socket object
// fail: reject a error
const initSocket = () => {
return new Promise((resolve, reject) => {
// create socket for communication
const socket = io("localhost:5000", {
"reconnection delay": 0,
"reopen delay": 0,
"force new connection": true
});
// define event handler for sucessfull connection
socket.on(ev.CONNECT, () => {
logger.info("connected");
resolve(socket);
});
// if connection takes longer than 5 seconds throw error
setTimeout(() => {
reject(new Error("Failed to connect wihtin 5 seconds."));
}, 5000);
}
);
};
// destroySocket returns a promise
// success: resolve true
// fail: resolve false
const destroySocket = socket => {
return new Promise((resolve, reject) => {
// check if socket connected
if (socket.connected) {
// disconnect socket
logger.info("disconnecting...");
socket.disconnect();
resolve(true);
} else {
// not connected
logger.info("no connection to break...");
resolve(false);
}
});
};
describe("test suit: Echo & Bello", () => {
test("test: ECHO", async () => {
// create socket for communication
const socketClient = await initSocket();
// create new promise for server response
const serverResponse = new Promise((resolve, reject) => {
// define a handler for the test event
socketClient.on(ev.res_ECHO, data4Client => {
//process data received from server
const { message } = data4Client;
logger.info("Server says: " + message);
// destroy socket after server responds
destroySocket(socketClient);
// return data for testing
resolve(data4Client);
});
// if response takes longer than 5 seconds throw error
setTimeout(() => {
reject(new Error("Failed to get reponse, connection timed out..."));
}, 5000);
});
// define data 4 server
const data4Server = { message: "CLIENT ECHO" };
// emit event with data to server
logger.info("Emitting ECHO event");
socketClient.emit(ev.com_ECHO, data4Server);
// wait for server to respond
const { status, message } = await serverResponse;
// check the response data
expect(status).toBe(200);
expect(message).toBe("SERVER ECHO");
});
test("test BELLO", async () => {
const socketClient = await initSocket();
const serverResponse = new Promise((resolve, reject) => {
socketClient.on(ev.res_BELLO, data4Client => {
const { message } = data4Client;
logger.info("Server says: " + message);
destroySocket(socketClient);
resolve(data4Client);
});
setTimeout(() => {
reject(new Error("Failed to get reponse, connection timed out..."));
}, 5000);
});
const data4Server = { message: "CLIENT BELLO" };
logger.info("Emitting BELLO event");
socketClient.emit(ev.com_BELLO, data4Server);
const { status, message } = await serverResponse;
expect(status).toBe(200);
expect(message).toBe("SERVER BELLO");
});
});
---- Foot Note ----
Depending on how you setup your server environment, you may experience environmental conflict between socket.io and socket.io-client running from the same project simultaneously. In which case it would be better to separate the project into a "test client" and a server. Checkout below repo if you get this issue.
https://github.com/PatMan10/testing_socketIO_server_v2
In OP's code,
socket.on('connect', function(done) {
console.log('worked...');
done();
});
the done was applied to the wrong callback. It should be removed from the socket.on callback and added to Mocha's it block callback:
it('First (hopefully useful) test', function (done) {
var socket = io.connect('http://localhost:3001');
socket.on('connect', function () {
console.log('worked...');
done();
});
});
A complete example
Existing answers are great but don't show the server ultimately being tested. Here's a complete version with console.logs to illustrate what's going on. Explanation follows.
src/server.js:
const express = require("express");
const createServer = (port=3000) => {
const app = express();
const http = require("http").Server(app);
const io = require("socket.io")(http);
io.on("connection", socket => {
console.log("[server] user connected");
socket.on("message", msg => {
console.log(`[server] received '${msg}'`);
socket.emit("message", msg);
});
socket.on("disconnect", () => {
console.log("[server] user disconnected");
});
});
http.listen(port, () =>
console.log(`[server] listening on port ${port}`)
);
return {
close: () => http.close(() =>
console.log("[server] closed")
)
};
};
module.exports = {createServer};
test/server.test.js:
const {expect} = require("chai");
const io = require("socket.io-client");
const {createServer} = require("../src/server");
const socketUrl = "http://localhost:3000";
describe("server", function () {
this.timeout(3000);
let server;
let sockets;
beforeEach(() => {
sockets = [];
server = createServer();
});
afterEach(() => {
sockets.forEach(e => e.disconnect())
server.close();
});
const makeSocket = (id=0) => {
const socket = io.connect(socketUrl, {
"reconnection delay": 0,
"reopen delay": 0,
"force new connection": true,
transports: ["websocket"],
});
socket.on("connect", () => {
console.log(`[client ${id}] connected`);
});
socket.on("disconnect", () => {
console.log(`[client ${id}] disconnected`);
});
sockets.push(socket);
return socket;
};
it("should echo a message to a client", done => {
const socket = makeSocket();
socket.emit("message", "hello world");
socket.on("message", msg => {
console.log(`[client] received '${msg}'`);
expect(msg).to.equal("hello world");
done();
});
});
it("should echo messages to multiple clients", () => {
const sockets = [...Array(5)].map((_, i) => makeSocket(i));
return Promise.all(sockets.map((socket, id) =>
new Promise((resolve, reject) => {
const msgs = [..."abcd"].map(e => e + id);
msgs.slice().forEach(e => socket.emit("message", e));
socket.on("message", msg => {
console.log(`[client ${id}] received '${msg}'`);
expect(msg).to.equal(msgs.shift());
if (msgs.length === 0) {
resolve();
}
});
})
));
});
});
In summary, the server exports a function that lets a server app be created from scratch, allowing each it block to be idempotent and avoid server state from carrying between tests (assuming no persistence on the server otherwise). Creating an app returns an object with a close function. socket.disconnect() must be called per socket in each test to avoid timeouts.
Given these requirements, the testing suite follows this per-test setup/teardown workflow:
let server;
let sockets;
beforeEach(() => {
sockets = [];
server = createServer();
});
afterEach(() => {
sockets.forEach(e => e.disconnect())
server.close();
});
makeSocket is an optional helper to reduce the repeated boilerplate of connecting and disconnecting a socket client. It does produce a side effect on the sockets array for cleanup later, but this is an implementation detail from the it block's perspective. Test blocks shoudn't touch server or sockets variables, although other workflows are likely depending on need. The critical takeaways are test case idempotency and closing all connections after each test case.
Options on the socket.connect object on the client let you choose transport and behavior of the socket. "force new connection": true creates a new Manager per socket instead of reusing an existing one and transports: ["websocket"] upgrades to WS protocol from long polling immediately.
Use it("should ... ", done => { /* tests */ }); and invoke done() after all work is completed in callbacks or return a promise (and omit the done parameter to the it callback). The example above shows both approaches.
Used in this post:
node: 12.19.0
chai: 4.2.0
express: 4.16.4
mocha: 5.2.0
socket.io: 2.2.0
socket.io-client: 2.2.0
I had this problem: How to do unit test with a "socket.io-client" if you don't know how long the server take to respond?.
I've solved so using mocha and chai:
var os = require('os');
var should = require("chai").should();
var socketio_client = require('socket.io-client');
var end_point = 'http://' + os.hostname() + ':8081';
var opts = {forceNew: true};
describe("async test with socket.io", function () {
this.timeout(10000);
it('Response should be an object', function (done) {
setTimeout(function () {
var socket_client = socketio_client(end_point, opts);
socket_client.emit('event', 'ABCDEF');
socket_client.on('event response', function (data) {
data.should.be.an('object');
socket_client.disconnect();
done();
});
socket_client.on('event response error', function (data) {
console.error(data);
socket_client.disconnect();
done();
});
}, 4000);
});
});