__tests__/index.test.js
const assert = require('assert');
const http = require('http');
const https = require('https');
const { Hub, HubClient } = require('../index');
const httpShutdown = require('http-shutdown');
const WebSocket = require('ws');
const { delay, delayUntil } = require('../helpers/delay');
const { checkHasClientId } = require('../lib/clientId');
const fs = require('fs');
const path = require('path');
describe('Hub', () => {
it('should return a class function', () => {
assert.strictEqual(typeof Hub, 'function');
assert(Hub instanceof Object);
assert.strictEqual(
Object.getOwnPropertyNames(Hub).includes('arguments'),
false
);
});
describe('an instance of Hub', () => {
const hub = new Hub({ port: 4000 });
it('should initialize a http server by default', () => {
assert(hub.server);
});
it('should initialize a websocket server by default', () => {
assert(hub.wss);
});
it('should attach event listener bindings to the websocket server', () => {
assert(hub.serverEventListeners.connection.length === 1);
assert(hub.serverEventListeners.listening.length === 0);
assert(hub.serverEventListeners.headers.length === 0);
assert(hub.serverEventListeners.error.length === 0);
assert(hub.serverEventListeners.close.length === 0);
assert(hub.connectionEventListeners.message.length > 0);
assert(hub.connectionEventListeners.error.length === 0);
assert(hub.connectionEventListeners.close.length === 1);
assert.strictEqual(hub.wss._eventsCount, 5);
});
describe('#listen', () => {
let runningServer = httpShutdown(hub.listen());
after(() => {
runningServer.shutdown();
});
it('should listen on the given port, and return the server', async () => {
let connected = false;
const client = new WebSocket('ws://localhost:4000');
client.onopen = () => {
connected = true;
};
await delayUntil(() => client.readyState === 1);
assert(connected);
client.close();
});
it('should attach the connection event listeners', async () => {
let connected = false;
const messages = [];
const client = new WebSocket('ws://localhost:4000');
client.onopen = () => {
connected = true;
};
client.onmessage = (event) => {
messages.push(JSON.parse(event.data));
};
await delayUntil(() => client.readyState === 1);
assert(connected);
const latestMessage = messages[messages.length - 1];
assert(latestMessage.action === 'get-client-id');
client.send(
JSON.stringify({
action: 'get-client-id',
data: { clientId: null },
})
);
client.close();
});
it('should attach the hasClientId rpc action', () => {
assert.deepStrictEqual(hub.rpc.actions['has-client-id'], [
checkHasClientId,
]);
});
});
});
describe('initialising with redis data store options', () => {
let hub;
let hubClient;
before(() => {
hub = new Hub({
port: 4005,
dataStoreType: 'redis',
dataStoreOptions: {
redisConfig: {
db: 1,
},
},
});
hub.listen();
hubClient = new HubClient({ url: 'ws://localhost:4005' });
});
after(async () => {
const { redis, channelsKey, clientsKey } = hub.pubsub.dataStore;
await redis.del(channelsKey);
await redis.del(clientsKey);
hubClient.sarus.disconnect();
hub.server.close();
// We delay so that the client can be unsubscribed
await delay(100);
try {
await hub.pubsub.dataStore.internalRedis.quit();
await hub.pubsub.dataStore.redis.quit();
} catch (err) {
console.error(err);
}
});
it('should have a redis client', () => {
assert(hub.pubsub.dataStore.redis);
assert.strictEqual(hub.pubsub.dataStore.redis._eventsCount, 0);
});
it('should handle subscribing a client to a channel', async () => {
await hubClient.isReady();
await delayUntil(() => hubClient.getClientId() !== null);
const response = await hubClient.subscribe('news');
if (!response.success) {
console.error(response);
}
assert(response.success);
});
it('should handle a client publishing a message to a channel', async () => {
let called = false;
hubClient.addChannelMessageHandler('news', () => {
called = true;
});
await hubClient.publish('news', 'rain is on the way');
await delayUntil(() => called);
});
it('should handle the server publishing a message to a channel', async () => {
let called = false;
hubClient.addChannelMessageHandler('news', () => {
called = true;
});
await hub.pubsub.publish({
data: { channel: 'news', message: 'rain is on the way' },
});
await delayUntil(() => called, 5050);
});
it('should handle unsubscribing a client from a channel', async () => {
const response = await hubClient.unsubscribe('news');
assert(response.success);
});
});
describe('when a client disconnects from the server', () => {
it('should unsubscribe that client from any channels they were subscribed to', async () => {
const newHub = await new Hub({
port: 5002,
dataStoreType: 'memory',
});
newHub.listen();
const hubClient = new HubClient({ url: 'ws://localhost:5002' });
// await delayUntil(() => hubClient.sarus.ws.readyState === 1);
// await delayUntil(() => {
// return hubClient.getClientId();
// });
await hubClient.isReady();
await hubClient.subscribe('accounts');
hubClient.sarus.disconnect();
await delay(100);
const channels = await newHub.pubsub.dataStore.getChannelsForClientId(
hubClient.getClientId()
);
const clientIds = await newHub.pubsub.dataStore.getClientIdsForChannel(
'accounts'
);
assert.deepStrictEqual(channels, []);
assert.deepStrictEqual(clientIds, []);
newHub.server.close();
});
});
describe('server option', () => {
const serverOptions = {
key: fs.readFileSync(
path.join(process.cwd(), 'certs', 'localhost+2-key.pem')
),
cert: fs.readFileSync(
path.join(process.cwd(), 'certs', 'localhost+2.pem')
),
};
describe('when no option is passed', () => {
it('should load a http server by default', async () => {
const plainHub = await new Hub({ port: 5003 });
assert(plainHub.server instanceof http.Server);
assert.strictEqual(plainHub.protocol, 'ws');
});
});
describe('when http is passed', () => {
it('should load a http server', async () => {
const plainHub = await new Hub({ port: 5003, server: 'http' });
assert(plainHub.server instanceof http.Server);
assert.strictEqual(plainHub.protocol, 'ws');
});
});
describe('when a http server is passed', () => {
it('should load that http server', async () => {
const httpServer = http.createServer();
const plainHub = await new Hub({
port: 5003,
server: httpServer,
});
assert(plainHub.server instanceof http.Server);
assert.deepStrictEqual(plainHub.server, httpServer);
assert.strictEqual(plainHub.protocol, 'ws');
});
});
describe('when https is passed', () => {
it('should load a https server initialialised with the serverOptions', async () => {
const secureHub = await new Hub({
port: 5003,
server: 'https',
serverOptions,
});
assert(secureHub.server instanceof https.Server);
assert.strictEqual(secureHub.protocol, 'wss');
});
});
describe('when a https server is passed', () => {
it('should load that https server', async () => {
const httpsServer = https.createServer(serverOptions);
const secureHub = await new Hub({
port: 5003,
server: httpsServer,
});
assert(secureHub.server instanceof https.Server);
assert.deepStrictEqual(secureHub.server, httpsServer);
assert.strictEqual(secureHub.protocol, 'wss');
});
});
describe('when an invalid server option is passed', () => {
it('should throw an error', async () => {
try {
await new Hub({ port: 5003, server: 'secure' });
assert(false, 'Should not reach this point');
} catch (err) {
assert.strictEqual(
err.message,
'Invalid option passed for server'
);
}
});
});
});
describe('setHostAndIp', () => {
let hub;
let hubClient;
before(() => {
hub = new Hub({
port: 4009,
});
hub.listen();
hubClient = new HubClient({ url: 'ws://localhost:4009' });
});
after(async () => {
hubClient.sarus.disconnect();
hub.server.close();
await delay(100);
});
it('should set the hostname and ip address on the websocket client', async () => {
await hubClient.isReady();
const ipAddress = '::1';
const ws = Array.from(hub.wss.clients)[0];
assert.strictEqual(ws.host, 'localhost:4009');
assert.strictEqual(ws.ipAddress, ipAddress);
});
});
describe('kick', () => {
let hub;
let hubClient;
const messages = [];
before(async () => {
hub = new Hub({
port: 4010,
});
hub.listen();
hubClient = new HubClient({ url: 'ws://localhost:4010' });
hubClient.sarus.on('message', (event) => {
const message = JSON.parse(event.data);
messages.push(message);
});
await hubClient.isReady();
const ws = Array.from(hub.wss.clients)[0];
await hub.kick({ ws });
await delay(100);
});
after(async () => {
hub.server.close();
await delay(25);
});
it('should send a RPC action to the client to stop them from automatically reconnecting', async () => {
const lastMessage = messages[messages.length - 1];
assert.strictEqual(lastMessage.type, 'request');
assert.strictEqual(lastMessage.action, 'kick');
assert.strictEqual(
lastMessage.data,
'Server has kicked the client'
);
});
it('should then close the websocket connection to the client', async () => {
assert.strictEqual(hubClient.sarus.ws.readyState, 3);
});
});
describe('kickAndBan', () => {
let hub;
let hubClient;
let ws;
before(async () => {
hub = new Hub({
port: 4011,
});
hub.listen();
hubClient = new HubClient({ url: 'ws://localhost:4011' });
await hubClient.isReady();
ws = Array.from(hub.wss.clients)[0];
await hub.kickAndBan({ ws });
await delay(100);
});
it('should add the client to the ban list', async () => {
const { clientId, host, ipAddress } = ws;
assert(ws.clientId !== null);
await delayUntil(async () => {
const hasBeenBanned = await hub.dataStore.hasBanRule({
clientId,
host,
ipAddress,
});
return hasBeenBanned;
});
});
it('should then kick the client', () => {
assert.strictEqual(hubClient.sarus.ws.readyState, 3);
});
});
describe('kickIfBanned', () => {
let hub;
let hubClient;
let ws;
before(async () => {
hub = new Hub({
port: 4012,
});
hub.listen();
});
describe('when the client is not banned', () => {
it('should allow the client to proceed', async () => {
hubClient = new HubClient({ url: 'ws://localhost:4012' });
await hubClient.isReady();
ws = Array.from(hub.wss.clients)[0];
await delay(100);
assert.strictEqual(hubClient.sarus.ws.readyState, 1);
});
});
describe('when the client is banned', () => {
it('should not allow the client to proceed, and kick them off', async () => {
await hub.kickAndBan({ ws });
await delay(100);
hubClient.sarus.connect();
await delay(100);
assert.strictEqual(hubClient.sarus.ws.readyState, 3);
});
});
});
});