anephenix/hub

View on GitHub
__tests__/lib/pubsub.test.js

Summary

Maintainability
F
4 days
Test Coverage
// Dependencies
const assert = require('assert');
const { Hub, HubClient } = require('../../index');
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
const { delay, delayUntil } = require('../../helpers/delay');
const MemoryDataStore = require('../../lib/dataStores/memory');
const httpShutdown = require('http-shutdown');

describe('pubsub', () => {
    let hub;
    let server;

    before(async () => {
        hub = new Hub({ port: 5050 });
        server = httpShutdown(hub.server);
        server.listen(5050);
    });

    after((done) => {
        server.shutdown(done);
    });

    describe('#subscribe', () => {
        describe('when passed a clientId and a channel', () => {
            it('should add a client to a channel', async () => {
                const data = { channel: 'sport' };
                const socket = {
                    clientId: 'xxxx',
                };
                const secondWs = {
                    clientId: 'wwww',
                };
                const secondData = { channel: 'business' };
                const response = await hub.pubsub.subscribe({ data, socket });
                assert(response.success);
                assert.strictEqual(
                    response.message,
                    'Client "xxxx" subscribed to channel "sport"'
                );
                assert.deepStrictEqual(hub.pubsub.dataStore.channels.sport, [
                    'xxxx',
                ]);
                assert.deepStrictEqual(hub.pubsub.dataStore.clients.xxxx, [
                    'sport',
                ]);
                await hub.pubsub.subscribe({ data, socket: secondWs });
                await hub.pubsub.subscribe({ data: secondData, socket });
                assert.deepStrictEqual(hub.pubsub.dataStore.channels.sport, [
                    'xxxx',
                    'wwww',
                ]);
                assert.deepStrictEqual(hub.pubsub.dataStore.clients.wwww, [
                    'sport',
                ]);
                assert.deepStrictEqual(hub.pubsub.dataStore.channels.business, [
                    'xxxx',
                ]);
                assert.deepStrictEqual(hub.pubsub.dataStore.clients.xxxx, [
                    'sport',
                    'business',
                ]);
            });
        });

        describe('when the websocket does not have a client id', () => {
            it('should throw an error indicating that the websocket does not have an id', async () => {
                const data = { channel: 'weather' };
                const socket = {};
                assert.rejects(
                    async () => {
                        await hub.pubsub.subscribe({ data, socket });
                    },
                    { message: 'No client id was found on the WebSocket' }
                );
            });
        });

        describe('when the channel is not passed', () => {
            it('should throw an error indicating that the channel was not passed', async () => {
                const data = {};
                const socket = {
                    clientId: 'yyyy',
                };
                assert.rejects(
                    async () => {
                        await hub.pubsub.subscribe({ data, socket });
                    },
                    { message: 'No channel was passed in the data' }
                );
            });
        });

        describe('when the channel has been added by the server with options', () => {
            it('should run a check against the authenticate function set for that channel', async () => {
                const channel = 'fish';
                let called = false;
                const authenticate = ({ data, socket }) => {
                    assert.strictEqual(data.password, 'food');
                    assert.strictEqual(socket.clientId, 'ooo');
                    called = true;
                    return called;
                };
                hub.pubsub.addChannelConfiguration({ channel, authenticate });

                const data = {
                    channel,
                    password: 'food',
                };
                const socket = {
                    clientId: 'ooo',
                };
                await hub.pubsub.subscribe({ data, socket });
                assert(called);
                const channels = await hub.pubsub.dataStore.getChannelsForClientId(
                    socket.clientId
                );
                assert(channels.indexOf('fish') !== -1);
            });
        });

        describe('when a client makes multiple attempts to subscribe to the same channel', () => {
            it('should only record a single entry of the client id in the channel subscriptions, and vice versa', async () => {
                const data = { channel: 'entertainment' };
                const socket = {
                    clientId: 'zzzz',
                };
                const firstResponse = await hub.pubsub.subscribe({
                    data,
                    socket,
                });
                const secondResponse = await hub.pubsub.subscribe({
                    data,
                    socket,
                });
                assert(firstResponse.success);
                assert(secondResponse.success);
                const message =
                    'Client "zzzz" subscribed to channel "entertainment"';
                assert.strictEqual(firstResponse.message, message);
                assert.strictEqual(secondResponse.message, message);
                assert.deepStrictEqual(
                    hub.pubsub.dataStore.channels.entertainment,
                    ['zzzz']
                );
                assert.deepStrictEqual(hub.pubsub.dataStore.clients.zzzz, [
                    'entertainment',
                ]);
            });
        });
    });

    describe('#publish', () => {
        it('should allow the client to publish a message to all of the channel subscribers, including themselves', async () => {
            const messages = [];
            const hubClient = new HubClient({ url: 'ws://localhost:5050' });
            hubClient.sarus.on('message', (event) => {
                const message = JSON.parse(event.data);
                messages.push(message);
            });
            await hubClient.isReady();
            // Subscribe the client to the channel
            await hubClient.subscribe('politics');
            // Acknowledge the channel subscription
            const clientId = global.localStorage.getItem('sarus-client-id');
            const latestMessage = messages[messages.length - 1];
            if (!latestMessage) throw new Error('No messages intercepted');
            assert.strictEqual(latestMessage.data.success, true);
            assert.strictEqual(
                latestMessage.data.message,
                `Client "${clientId}" subscribed to channel "politics"`
            );

            // Get the client to publish a message to the channel
            await hubClient.publish('politics', 'Elections held');
            // Check that the client receives the messages
            const thePreviousLatestMessage = messages[messages.length - 2];
            const theNextLatestMessage = messages[messages.length - 1];
            assert.strictEqual(thePreviousLatestMessage.action, 'message');
            assert.strictEqual(
                thePreviousLatestMessage.data.channel,
                'politics'
            );
            assert.strictEqual(
                thePreviousLatestMessage.data.message,
                'Elections held'
            );
            assert.strictEqual(theNextLatestMessage.action, 'publish');
            assert.strictEqual(theNextLatestMessage.data.success, true);
            hubClient.sarus.disconnect();
        });

        it('should allow the client to publish a message to all of the channel subscribers, excluding themselves', async () => {
            const messages = [];
            const hubClient = new HubClient({ url: 'ws://localhost:5050' });
            hubClient.sarus.on('message', (event) => {
                const message = JSON.parse(event.data);
                messages.push(message);
            });
            await hubClient.isReady();
            // Subscribe the client to the channel
            await hubClient.subscribe('showbiz');
            // Acknowledge the channel subscription
            const clientId = global.localStorage.getItem('sarus-client-id');
            const latestMessage = messages[messages.length - 1];
            if (!latestMessage) throw new Error('No messages intercepted');
            assert.strictEqual(latestMessage.data.success, true);
            assert.strictEqual(
                latestMessage.data.message,
                `Client "${clientId}" subscribed to channel "showbiz"`
            );

            // Get the client to publish a message to the channel
            await hubClient.publish(
                'showbiz',
                'Oscars ceremony to be virtual',
                true
            );
            // Check that the client does not receive the message
            const thePreviousLatestMessage = messages[messages.length - 2];
            const theNextLatestMessage = messages[messages.length - 1];
            assert.notStrictEqual(thePreviousLatestMessage.action, 'message');
            assert.strictEqual(thePreviousLatestMessage.action, 'subscribe');
            assert.strictEqual(theNextLatestMessage.action, 'publish');
            assert.strictEqual(theNextLatestMessage.data.success, true);
            assert.strictEqual(
                theNextLatestMessage.data.message,
                'Published message'
            );
            hubClient.sarus.disconnect();
        });

        it('should allow the server to publish a message to all of the channel subscribers', async () => {
            const messages = [];
            const hubClient = new HubClient({ url: 'ws://localhost:5050' });
            hubClient.sarus.on('message', (event) => {
                const message = JSON.parse(event.data);
                messages.push(message);
            });
            await hubClient.isReady();
            // Subscribe the client to the channel
            await hubClient.subscribe('markets');
            // Acknowledge the channel subscription
            const clientId = global.localStorage.getItem('sarus-client-id');
            const latestMessage = messages[messages.length - 1];
            if (!latestMessage) throw new Error('No messages intercepted');
            assert.strictEqual(latestMessage.data.success, true);
            assert.strictEqual(
                latestMessage.data.message,
                `Client "${clientId}" subscribed to channel "markets"`
            );
            // Get the server to publish a message to the channel
            await hub.pubsub.publish({
                data: { channel: 'markets', message: 'FTSE: 5845 (-5)' },
            });
            await delay(25);
            // Check that the client receives the message
            const theNextLatestMessage = messages[messages.length - 1];
            assert.strictEqual(theNextLatestMessage.action, 'message');
            assert.strictEqual(theNextLatestMessage.data.channel, 'markets');
            assert.strictEqual(
                theNextLatestMessage.data.message,
                'FTSE: 5845 (-5)'
            );
            hubClient.sarus.disconnect();
        });

        describe('when publishing from a client', () => {
            it('should return an error response if the websocket client id is not present', async () => {
                const messages = [];
                const client = new WebSocket('ws://localhost:5050');
                client.on('message', (event) => {
                    const message = JSON.parse(event);
                    messages.push(message);
                });
                const publishRequest = {
                    id: uuidv4(),
                    action: 'publish',
                    type: 'request',
                    data: {
                        channel: 'sport',
                        message: 'Something is happening',
                    },
                };
                await delayUntil(() => client.readyState === 1);
                client.send(JSON.stringify(publishRequest));
                await delay(50);
                const latestMessage = messages[messages.length - 1];
                assert.strictEqual(latestMessage.type, 'error');
                assert.strictEqual(
                    latestMessage.error,
                    'No client id was found on the WebSocket'
                );
                client.close();
            });
            it('should return an error response if the channel is missing', async () => {
                const messages = [];
                const hubClient = new HubClient({ url: 'ws://localhost:5050' });
                hubClient.sarus.on('message', (event) => {
                    const message = JSON.parse(event.data);
                    messages.push(message);
                });
                await hubClient.isReady();
                // Subscribe the client to the channel
                await hubClient.subscribe('showbiz');
                // Acknowledge the channel subscription
                const clientId = global.localStorage.getItem('sarus-client-id');
                const latestMessage = messages[messages.length - 1];
                if (!latestMessage) throw new Error('No messages intercepted');
                assert.strictEqual(latestMessage.data.success, true);
                assert.strictEqual(
                    latestMessage.data.message,
                    `Client "${clientId}" subscribed to channel "showbiz"`
                );

                // Get the client to publish a message to the channel
                await hubClient.publish(null, 'Oscars ceremony to be virtual');
                // Check that the client receives the message
                const theNextLatestMessage = messages[messages.length - 1];
                assert.strictEqual(theNextLatestMessage.type, 'error');
                assert.strictEqual(
                    theNextLatestMessage.error,
                    'No channel was passed in the data'
                );
                hubClient.sarus.disconnect();
            });
            it('should return an error response if the message is missing', async () => {
                const messages = [];
                const hubClient = new HubClient({ url: 'ws://localhost:5050' });
                hubClient.sarus.on('message', (event) => {
                    const message = JSON.parse(event.data);
                    messages.push(message);
                });
                await hubClient.isReady();
                // Subscribe the client to the channel
                await hubClient.subscribe('showbiz');
                // Acknowledge the channel subscription
                const clientId = global.localStorage.getItem('sarus-client-id');
                const latestMessage = messages[messages.length - 1];
                if (!latestMessage) throw new Error('No messages intercepted');
                assert.strictEqual(latestMessage.data.success, true);
                assert.strictEqual(
                    latestMessage.data.message,
                    `Client "${clientId}" subscribed to channel "showbiz"`
                );
                // Get the client to publish a message to the channel
                await hubClient.publish('showbiz', null);
                // Check that the client receives the message
                const theNextLatestMessage = messages[messages.length - 1];
                assert.strictEqual(theNextLatestMessage.type, 'error');
                assert.strictEqual(
                    theNextLatestMessage.error,
                    'No message was passed in the data'
                );
                hubClient.sarus.disconnect();
            });

            it('should not allow a client to publish to a channel that they are not subscribed to', async () => {
                const messages = [];
                const hubClient = new HubClient({ url: 'ws://localhost:5050' });
                hubClient.sarus.on('message', (event) => {
                    const message = JSON.parse(event.data);
                    messages.push(message);
                });
                await hubClient.isReady();
                const latestMessage = messages[messages.length - 1];
                if (!latestMessage) throw new Error('No messages intercepted');
                // Get the client to publish a message to the channel
                await hubClient.publish('dashboard_y', 'Some data');
                // Check that the client receives the message
                const theNextLatestMessage = messages[messages.length - 1];
                assert.strictEqual(theNextLatestMessage.type, 'error');
                assert.strictEqual(
                    theNextLatestMessage.error,
                    'You must subscribe to the channel to publish messages to it'
                );
                hubClient.sarus.disconnect();
            });
        });

        describe('when publishing from a server', () => {
            it('should return an error response if the channel is missing', async () => {
                assert.rejects(
                    async () => {
                        await hub.pubsub.publish({
                            data: {
                                message: 'FTSE: 5845 (-5)',
                            },
                        });
                    },
                    { message: 'No channel was passed in the data' }
                );
            });

            it('should return an error response if the message is missing', async () => {
                assert.rejects(
                    async () => {
                        await hub.pubsub.publish({
                            data: {
                                channel: 'markets',
                            },
                        });
                    },
                    { message: 'No message was passed in the data' }
                );
            });

            describe('publishing to a channel that has no subscribers', () => {
                it('should publish the message, even if there are no subscribers for that channel', async () => {
                    await hub.pubsub.publish({
                        data: {
                            channel: 'dashboard_x',
                            message: 'FTSE: 5845 (-5)',
                        },
                    });
                });
            });
        });

        describe('when using the redis dataStore', () => {
            let firstHub;
            let secondHub;
            let firstHubClient;
            let secondHubClient;

            before(async () => {
                firstHub = new Hub({ port: 4006, dataStoreType: 'redis' });
                secondHub = new Hub({ port: 4007, dataStoreType: 'redis' });
                firstHub.listen();
                secondHub.listen();
                firstHubClient = new HubClient({ url: 'ws://localhost:4006' });
                secondHubClient = new HubClient({ url: 'ws://localhost:4007' });
                await delayUntil(
                    () =>
                        firstHubClient.getClientId() !== null &&
                        secondHubClient.getClientId() !== null
                );
            });

            after(async () => {
                firstHubClient.sarus.disconnect();
                secondHubClient.sarus.disconnect();
                firstHub.server.close();
                secondHub.server.close();
                await delay(100);
                await firstHub.pubsub.dataStore.redis.quit();
                await secondHub.pubsub.dataStore.redis.quit();
                await firstHub.pubsub.dataStore.internalRedis.quit();
                await secondHub.pubsub.dataStore.internalRedis.quit();
            });

            it('should relay the published message to all Hub server instances via Redis', async () => {
                let firstClientMessage;
                let firstClientReceivesMessage = false;
                let secondClientMessage;
                let secondClientReceivesMessage = false;
                const firstClientHandlerFunction = (message) => {
                    firstClientMessage = message;
                    firstClientReceivesMessage = true;
                };
                firstHubClient.addChannelMessageHandler(
                    'news',
                    firstClientHandlerFunction
                );
                const secondClientHandlerFunction = (message) => {
                    secondClientMessage = message;
                    secondClientReceivesMessage = true;
                };
                secondHubClient.addChannelMessageHandler(
                    'news',
                    secondClientHandlerFunction
                );
                await firstHubClient.subscribe('news');
                await secondHubClient.subscribe('news');
                const message = 'Sunny weather on the way';
                await firstHub.pubsub.publish({
                    data: {
                        channel: 'news',
                        message,
                    },
                });
                await delayUntil(
                    () =>
                        firstClientReceivesMessage &&
                        secondClientReceivesMessage
                );
                assert.strictEqual(firstClientMessage, message);
                assert.strictEqual(secondClientMessage, message);
            });
        });
    });

    describe('#unsubscribe', () => {
        it('should remove a client from a channel, and ensure that the client no longer receives messages for that channel', async () => {
            const messages = [];
            const config = { url: 'ws://localhost:5050' };
            const hubClient = new HubClient(config);
            hubClient.sarus.on('message', (event) => {
                const message = JSON.parse(event.data);
                messages.push(message);
            });
            await hubClient.isReady();
            // Subscribe the client to the channel
            await hubClient.subscribe('markets');
            // Acknowledge the channel subscription
            const clientId = global.localStorage.getItem('sarus-client-id');
            const latestMessage = messages[messages.length - 1];
            if (!latestMessage) throw new Error('No messages intercepted');
            assert.strictEqual(latestMessage.data.success, true);
            assert.strictEqual(
                latestMessage.data.message,
                `Client "${clientId}" subscribed to channel "markets"`
            );
            // Subscribe the client to the channel
            await hubClient.unsubscribe('markets');
            const theNextLatestMessage = messages[messages.length - 1];
            assert.strictEqual(theNextLatestMessage.data.success, true);
            assert.strictEqual(
                theNextLatestMessage.data.message,
                `Client "${clientId}" unsubscribed from channel "markets"`
            );
            // Get the server to publish a message to the channel

            // a second client needs to be subscribed, and localstorage scrubbed to prevent duplicate client id assignment;
            global.localStorage.removeItem('sarus-client-id');
            const otherHubClient = new HubClient(config);
            await otherHubClient.isReady();
            await otherHubClient.subscribe('markets');

            await hub.pubsub.publish({
                data: {
                    channel: 'markets',
                    message: 'FTSE: 5845 (-5)',
                },
            });
            await delay(25);

            // Check that the client does not receive the message
            const theFinalLatestMessage = messages[messages.length - 1];
            assert.notStrictEqual(theFinalLatestMessage.action, 'message');
            hubClient.sarus.disconnect();
            otherHubClient.sarus.disconnect();
        });

        it('should return an error response if the websocket client id is not present', async () => {
            const messages = [];
            const client = new WebSocket('ws://localhost:5050');
            client.on('message', (event) => {
                const message = JSON.parse(event);
                messages.push(message);
            });

            const unsubscribeRequest = {
                id: uuidv4(),
                action: 'unsubscribe',
                type: 'request',
                data: {
                    channel: 'sport',
                },
            };
            await delayUntil(() => client.readyState === 1);
            client.send(JSON.stringify(unsubscribeRequest));
            await delay(50);
            const latestMessage = messages[messages.length - 1];
            assert.strictEqual(latestMessage.type, 'error');
            assert.strictEqual(
                latestMessage.error,
                'No client id was found on the WebSocket'
            );
            client.close();
        });
        it('should return an error response if the channel is missing', async () => {
            const messages = [];
            const hubClient = new HubClient({ url: 'ws://localhost:5050' });
            hubClient.sarus.on('message', (event) => {
                const message = JSON.parse(event.data);
                messages.push(message);
            });
            await hubClient.isReady();
            // Subscribe the client to the channel
            await hubClient.subscribe('markets');
            // Acknowledge the channel subscription
            const clientId = global.localStorage.getItem('sarus-client-id');
            const latestMessage = messages[messages.length - 1];
            if (!latestMessage) throw new Error('No messages intercepted');
            assert.strictEqual(latestMessage.data.success, true);
            assert.strictEqual(
                latestMessage.data.message,
                `Client "${clientId}" subscribed to channel "markets"`
            );
            // Unsubscribe the client from the channel
            await hubClient.unsubscribe(null);
            const theNextLatestMessage = messages[messages.length - 1];
            assert.strictEqual(theNextLatestMessage.type, 'error');
            assert.strictEqual(
                theNextLatestMessage.error,
                'No channel was passed in the data'
            );
            hubClient.sarus.disconnect();
        });
    });

    describe('dataStore types', () => {
        it('should use the memory data store by default', () => {
            assert(hub.pubsub.dataStore instanceof MemoryDataStore);
        });

        describe('when passed a dataStoreType parameter', () => {
            describe('when the dataStore type exists', () => {
                it('should create an instance of that dataStore type and bind it to the class', async () => {
                    const redisHub = new Hub({
                        port: 6000,
                        dataStoreType: 'redis',
                    });
                    assert(redisHub.pubsub.dataStore.redis);
                    assert.strictEqual(
                        redisHub.pubsub.dataStore.redis._eventsCount,
                        0
                    );
                });
            });

            describe('when the dataStore type does not exist', () => {
                it('should throw an error', async () => {
                    assert.throws(
                        () => {
                            new Hub({ port: 6000, dataStoreType: 'postgres' });
                        },
                        {
                            message:
                                'dataStoreType "postgres" is not a valid option',
                        }
                    );
                });
            });
        });
    });

    describe('#unsubscribeClientFromAllChannels', () => {
        it('should unsubscribe the client from all of its channels', async () => {
            const newHub = await new Hub({ port: 5004 });
            newHub.listen();
            const hubClient = new HubClient({ url: 'ws://localhost:5004' });
            await hubClient.isReady();
            await hubClient.subscribe('shares');
            await newHub.pubsub.unsubscribeClientFromAllChannels({
                ws: { clientId: hubClient.getClientId() },
            });
            const channels = await newHub.pubsub.dataStore.getChannelsForClientId(
                hubClient.getClientId()
            );
            const clientIds = await newHub.pubsub.dataStore.getClientIdsForChannel(
                'shares'
            );
            assert.deepStrictEqual(channels, []);
            assert.deepStrictEqual(clientIds, []);
            hubClient.sarus.disconnect();
            newHub.server.close();
        });
    });

    describe('#addChannelConfiguration', () => {
        describe('when passed an authenticate option', () => {
            it('should add a channel with an authenticate function to call during subscription requests', async () => {
                const channel = 'dogs';
                let called = false;
                const authenticate = ({ data, socket }) => {
                    assert.strictEqual(data.password, 'food');
                    assert.strictEqual(socket.clientId, 'woof');
                    called = true;
                    return called;
                };
                hub.pubsub.addChannelConfiguration({ channel, authenticate });
                assert.deepStrictEqual(
                    hub.pubsub.channelConfigurations.dogs.authenticate,
                    authenticate
                );
                const data = {
                    channel,
                    password: 'food',
                };
                const socket = {
                    clientId: 'woof',
                };
                await hub.pubsub.subscribe({ data, socket });
                assert(called);
                const channels = await hub.pubsub.dataStore.getChannelsForClientId(
                    socket.clientId
                );
                assert(channels.indexOf('dogs') !== -1);
            });
        });

        describe('when passed a wildcard channel name', () => {
            it('should be able to run the channel configuration for channels that match the wildcard channel name', async () => {
                // create a channel configuration for multiple dashboards
                const wildcardChannel = 'dashboard_*';
                let callCount = 0;
                const authenticate = ({ data, socket }) => {
                    assert.strictEqual(data.password, 'opensesame');
                    assert.strictEqual(socket.clientId, 'viewer');
                    callCount++;
                    return true;
                };
                hub.pubsub.addChannelConfiguration({
                    channel: wildcardChannel,
                    authenticate,
                });
                const otherWildcardChannel = 'dash_*';
                let otherCallCount = 0;
                const otherAuthenticate = () => {
                    otherCallCount++;
                    return true;
                };
                hub.pubsub.addChannelConfiguration({
                    channel: otherWildcardChannel,
                    authenticate: otherAuthenticate,
                });
                assert.deepStrictEqual(
                    hub.pubsub.channelConfigurations[wildcardChannel]
                        .authenticate,
                    authenticate
                );
                const data = {
                    channel: 'dashboard_e220j92',
                    password: 'opensesame',
                };
                const socket = {
                    clientId: 'viewer',
                };
                await hub.pubsub.subscribe({ data, socket });
                assert(callCount === 1);
                const channels = await hub.pubsub.dataStore.getChannelsForClientId(
                    socket.clientId
                );
                assert(channels.indexOf('dashboard_e220j92') !== -1);
                const dataTwo = {
                    channel: 'dashboard_29jd92j',
                    password: 'opensesame',
                };
                await hub.pubsub.subscribe({ data: dataTwo, socket });
                assert(callCount === 2);
                const channelsTwo = await hub.pubsub.dataStore.getChannelsForClientId(
                    socket.clientId
                );
                assert(channelsTwo.indexOf('dashboard_29jd92j') !== -1);
                assert.strictEqual(otherCallCount, 0);
            });

            it('should prevent the developer from adding wildcard channels that might overlap in channel matches', async () => {
                // First case - long then short wildcard overlap
                const wildcardChannelOne = 'magazine_*';
                const wildcardChannelTwo = 'mag*';

                // Second case - short then long wildcard overlap
                const wildcardChannelThree = 'des*';
                const wildcardChannelFour = 'design*';
                const normalChannelFive = 'de';

                // Just an example
                const authenticate = ({ data }) => {
                    return data.password === 'xyz';
                };
                hub.pubsub.addChannelConfiguration({
                    channel: wildcardChannelOne,
                    authenticate,
                });
                assert.deepStrictEqual(
                    hub.pubsub.channelConfigurations[wildcardChannelOne]
                        .authenticate,
                    authenticate
                );
                assert.throws(
                    () => {
                        hub.pubsub.addChannelConfiguration({
                            channel: wildcardChannelTwo,
                            authenticate,
                        });
                    },
                    {
                        message: `Wildcard channel name too ambiguous - will collide with "${wildcardChannelOne}"`,
                    }
                );

                hub.pubsub.addChannelConfiguration({
                    channel: wildcardChannelThree,
                    authenticate,
                });
                assert.deepStrictEqual(
                    hub.pubsub.channelConfigurations[wildcardChannelThree]
                        .authenticate,
                    authenticate
                );
                assert.throws(
                    () => {
                        hub.pubsub.addChannelConfiguration({
                            channel: wildcardChannelFour,
                            authenticate,
                        });
                    },
                    {
                        message: `Wildcard channel name too ambiguous - will collide with "${wildcardChannelThree}"`,
                    }
                );
                assert.throws(
                    () => {
                        hub.pubsub.addChannelConfiguration({
                            channel: normalChannelFive,
                            authenticate,
                        });
                    },
                    {
                        message: `Wildcard channel name too ambiguous - will collide with "${wildcardChannelThree}"`,
                    }
                );
            });
        });

        describe('when passed a clientCanPublish value', () => {
            describe('and the value is true', () => {
                it('should allow the client to publish to the channel', async () => {
                    const channelAllowed = 'birds';
                    let messageReceived;
                    hub.pubsub.addChannelConfiguration({
                        channel: channelAllowed,
                        clientCanPublish: true,
                    });
                    const hubClient = new HubClient({
                        url: 'ws://localhost:5050',
                    });
                    await hubClient.isReady();
                    await hubClient.subscribe(channelAllowed);
                    hubClient.addChannelMessageHandler(
                        channelAllowed,
                        (message) => {
                            messageReceived = message;
                        }
                    );
                    await hubClient.publish(channelAllowed, 'hello everyone');
                    assert.strictEqual(messageReceived, 'hello everyone');
                });
            });

            describe('and the value is false', () => {
                it('should not allow the client to publish to the channel', async () => {
                    let messageReceived;
                    const channelNotAllowed = 'crocadiles';
                    hub.pubsub.addChannelConfiguration({
                        channel: channelNotAllowed,
                        clientCanPublish: false,
                    });
                    const hubClient = new HubClient({
                        url: 'ws://localhost:5050',
                    });
                    await hubClient.isReady();
                    await hubClient.subscribe(channelNotAllowed);
                    hubClient.addChannelMessageHandler(
                        channelNotAllowed,
                        (message) => {
                            messageReceived = message;
                        }
                    );
                    const response = await hubClient.publish(
                        channelNotAllowed,
                        'hello everyone'
                    );
                    assert.strictEqual(
                        response,
                        'Clients cannot publish to the channel'
                    );
                    assert.notStrictEqual(messageReceived, 'hello everyone');
                });
            });

            describe('and the value is a function', () => {
                it('should be passed the data and the socket', async () => {
                    const channelAllowed = 'lizards';
                    let messageReceived;
                    let dataPassed;
                    let socketPassed;
                    const clientCanPublish = ({ data, socket }) => {
                        dataPassed = data;
                        socketPassed = socket;
                        return true;
                    };
                    hub.pubsub.addChannelConfiguration({
                        channel: channelAllowed,
                        clientCanPublish,
                    });
                    const hubClient = new HubClient({
                        url: 'ws://localhost:5050',
                    });
                    await hubClient.isReady();
                    await hubClient.subscribe(channelAllowed);
                    hubClient.addChannelMessageHandler(
                        channelAllowed,
                        (message) => {
                            messageReceived = message;
                        }
                    );
                    await hubClient.publish(channelAllowed, 'hello everyone');
                    assert.strictEqual(messageReceived, 'hello everyone');
                    assert.strictEqual(dataPassed.channel, channelAllowed);
                    assert.strictEqual(dataPassed.message, 'hello everyone');
                    assert.strictEqual(
                        socketPassed.clientId,
                        hubClient.getClientId()
                    );
                });
            });
        });
    });

    describe('#removeChannelConfiguration', () => {
        it('should remove the channel configuration', () => {
            const channel = 'dogs';
            assert(hub.pubsub.channelConfigurations[channel]);
            hub.pubsub.removeChannelConfiguration(channel);
            assert(!hub.pubsub.channelConfigurations[channel]);
        });
    });
});