Skip to content

Commit 366ed3a

Browse files
CarlosFdezchrishanel
authored andcommitted
Fixing tests.js (#35)
* Split server into lib/server and server.js to separate responsibilities, allowing injecting custom twitch chat clients. Temp fix to test.js. A true fix would create an entire mocked twitch chat client rather than just bits and pieces, but that can happen in the future. * Run ava with serial flag Since the tests start socket.io servers, running them one after the other will hopefully prevent certain race conditions. * Another attempt: use express and the same port to test the rawmessagetests Travis might have a different way of handling addresses than locally on Windows.
1 parent cc47c50 commit 366ed3a

6 files changed

Lines changed: 205 additions & 182 deletions

File tree

lib/server.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
'use strict';
2+
3+
const config = require('./config');
4+
const log = require('./log');
5+
const app = require('express')();
6+
const server = require('http').Server(app); // eslint-disable-line new-cap
7+
const io = require('socket.io')(server);
8+
9+
app.get('/', (req, res) => {
10+
res.sendStatus(200);
11+
});
12+
13+
const HEARTBEAT_TIMEOUT = 15 * 1000;
14+
const authenticatedSockets = new WeakSet();
15+
16+
let client = null;
17+
let chatClient = null;
18+
19+
/**
20+
* Initializes the socket.io server.
21+
* @param {*} twitchClient The chat client to communicate with.
22+
*/
23+
function setupServer(twitchClient) {
24+
client = twitchClient;
25+
chatClient = twitchClient.chatClient;
26+
27+
io.on('connection', socket => {
28+
log.trace('Socket %s connected.', socket.id);
29+
30+
socket.on('authenticate', (key, fn) => {
31+
log.debug('Socket %s authenticating with key "%s"', socket.id, key);
32+
33+
if (authenticatedSockets.has(socket)) {
34+
log.debug('Already authenticated');
35+
fn('already authenticated');
36+
return;
37+
}
38+
39+
if (key === config.get('secretKey')) {
40+
log.debug('Accepted key');
41+
setupAuthenticatedSocket(socket);
42+
fn(null);
43+
} else {
44+
log.info('Rejected key "%s"', key);
45+
fn('invalid key');
46+
}
47+
});
48+
});
49+
50+
log.info('Socket.IO server initialized');
51+
52+
server.listen(config.get('port'));
53+
log.info('Streen running on http://localhost:%s', config.get('port'));
54+
}
55+
56+
module.exports = {app, io, HEARTBEAT_TIMEOUT, setupServer};
57+
58+
function setupAuthenticatedSocket(socket) {
59+
authenticatedSockets.add(socket);
60+
61+
/**
62+
* Join a Twitch chat channel.
63+
* @param {String} channel - The name of the channel to join. Do not include a leading "#" character.
64+
* @param {Function} fn - The callback to execute after successfully joining the channel.
65+
*/
66+
socket.on('join', (channel, fn) => {
67+
// callback optional
68+
fn = fn || (() => {});
69+
70+
// NOTE 2/1/2017: Rooms are only left when the socket itself is closed. Is this okay? Is this a leak?
71+
// Have the socket join the namespace for the channel in order to receive messages.
72+
const roomName = `channel:${channel}`;
73+
if (Object.keys(socket.rooms).indexOf(roomName) < 0) {
74+
log.trace('Socket %s joined room:', socket.id, roomName);
75+
socket.join(roomName);
76+
}
77+
78+
client.resetHeartbeat(channel);
79+
80+
if (chatClient.channels.indexOf(`#${channel}`) >= 0) {
81+
// Already in channel, invoke callback with the name
82+
fn(null, channel);
83+
} else {
84+
chatClient.join(channel).then(() => {
85+
fn(null, null);
86+
}).catch(error => {
87+
log.error(`Error attempting to join "${channel}" from join command.\n\t`, error);
88+
fn(error);
89+
});
90+
}
91+
});
92+
93+
/**
94+
* Send a message to a Twitch chat channel as the user specified in the config file.
95+
* @param {String} channel - The name of the channel to send a message to. Do not include a leading "#" character.
96+
* @param {String} message - The message to send.
97+
* @param {Function} fn - The callback to execute after successfully sending the message.
98+
*/
99+
socket.on('say', (channel, message, fn) => {
100+
chatClient.say(channel, message).then(() => {
101+
fn(null, null);
102+
}).catch(error => {
103+
log.error(`Error attempting to "say" in channel "${channel}".\n\tMessage: ${message}\n\t`, error);
104+
fn(error);
105+
});
106+
});
107+
108+
/**
109+
* Timeout a user in a Twitch chat channel for a given number of seconds.
110+
* @param {String} channel - The name of the channel to execute the timeout command in.
111+
* Do not include a leading "#" character.
112+
* @param {String} username - The name of the user to timeout.
113+
* @param {Number} seconds - The number of seconds to time the user out for.
114+
* @param {Function} fn - The callback to execute after successfully timing out the user.
115+
*/
116+
socket.on('timeout', (channel, username, seconds, fn) => {
117+
chatClient.timeout(channel, username, seconds).then(() => {
118+
fn(null, null);
119+
}).catch(error => {
120+
log.error(`Error attempting to timeout user "${username}" in channel "${channel}" for ${seconds} seconds.\n\t`, error);
121+
fn(error);
122+
});
123+
});
124+
125+
/**
126+
* Get the list of chat mods for a Twitch channel.
127+
* @param {String} channel - The Twitch channel to get a list of chat mods from
128+
* @param {Function} fn - The callback to execute after successfully obtaining the list of chat mods.
129+
*/
130+
socket.on('mods', (channel, fn) => {
131+
chatClient.mods(channel).then(mods => {
132+
fn(null, mods);
133+
}).catch(error => {
134+
log.error(`Error attempting to get list of mods in channel "${channel}".\n\t`, error);
135+
fn(error);
136+
});
137+
});
138+
139+
/**
140+
* Tell Streen that you wish for it to remain in this array of channels.
141+
* @param {Array.<string>} channels - The array of channel names. Do not include leading "#" characters.
142+
* @param {heartbeatCallback} fb - The callback to execute after the heartbeat has been registered.
143+
*/
144+
socket.on('heartbeat', (channels, fn) => {
145+
client.heartbeat(channels);
146+
fn(null, HEARTBEAT_TIMEOUT);
147+
});
148+
149+
/**
150+
* The type of callback to execute after a successful heartbeat request.
151+
* @callback heartbeatCallback
152+
* @param {Object} err - The error returned, if any.
153+
* @param {Number} heartbeatTimeout - How long to wait (in milliseconds) before sending the next heartbeat.
154+
* Heartbeats can be sent earlier or later if needed.
155+
* A siphon has up to (heartbeatTimeout * 2 + 1000) milliseconds to
156+
* send another heartbeat before it times out. In other words, it can only miss
157+
* one consecutive heartbeat.
158+
*/
159+
}

lib/twitch_chat.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ class TwitchChatClient {
229229
* @param {*} channels
230230
*/
231231
heartbeat(channels) {
232+
if (!this.connected) {
233+
return;
234+
}
235+
232236
const chatClient = this.chatClient;
233237

234238
// If we're not in any of these channels, join them.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
},
4646
"scripts": {
4747
"static": "eslint server.js lib/**/*.js",
48-
"test": "npm run static && nyc ava --timeout=20s",
48+
"test": "npm run static && nyc ava --timeout=20s --serial",
4949
"start": "node server.js",
5050
"create_migration": "pg-migrate create",
5151
"migrate": "pg-migrate up"

server.js

Lines changed: 12 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,8 @@
1-
'use strict';
2-
3-
const config = require('./lib/config');
41
const log = require('./lib/log');
2+
const config = require('./lib/config');
53
const format = require('util').format;
6-
const app = require('express')();
7-
const server = require('http').Server(app); // eslint-disable-line new-cap
8-
const io = require('socket.io')(server);
94
const TwitchChatClient = require('./lib/twitch_chat');
10-
11-
app.get('/', (req, res) => {
12-
res.sendStatus(200);
13-
});
14-
15-
const HEARTBEAT_TIMEOUT = 15 * 1000;
16-
const authenticatedSockets = new WeakSet();
17-
18-
module.exports = {app, io, HEARTBEAT_TIMEOUT};
5+
const {io, HEARTBEAT_TIMEOUT, setupServer} = require('./lib/server');
196

207
// Wait until we've defined module.exports before loading the Twitch IRC and Slack libs
218
const slack = (function () {
@@ -52,140 +39,18 @@ process.on('SIGINT', () => {
5239
// Create the TwitchChatClient, restrieve the internal tmi client, and connect to twitch
5340
const client = new TwitchChatClient(io, HEARTBEAT_TIMEOUT, slack.status.bind(slack));
5441
const chatClient = client.chatClient;
55-
client.connect().then(() => {
56-
setupServer();
57-
});
5842

5943
// register the twitch chat client to slack (if enabled)
6044
slack.register(chatClient);
6145

62-
function setupServer() {
63-
io.on('connection', socket => {
64-
log.trace('Socket %s connected.', socket.id);
65-
66-
socket.on('authenticate', (key, fn) => {
67-
log.debug('Socket %s authenticating with key "%s"', socket.id, key);
68-
69-
if (authenticatedSockets.has(socket)) {
70-
log.debug('Already authenticated');
71-
fn('already authenticated');
72-
return;
73-
}
74-
75-
if (key === config.get('secretKey')) {
76-
log.debug('Accepted key');
77-
setupAuthenticatedSocket(socket);
78-
fn(null);
79-
} else {
80-
log.info('Rejected key "%s"', key);
81-
fn('invalid key');
82-
}
83-
});
84-
});
85-
86-
log.info('Socket.IO server initialized');
87-
88-
server.listen(config.get('port'));
89-
log.info('Streen running on http://localhost:%s', config.get('port'));
90-
}
91-
92-
function setupAuthenticatedSocket(socket) {
93-
authenticatedSockets.add(socket);
94-
95-
/**
96-
* Join a Twitch chat channel.
97-
* @param {String} channel - The name of the channel to join. Do not include a leading "#" character.
98-
* @param {Function} fn - The callback to execute after successfully joining the channel.
99-
*/
100-
socket.on('join', (channel, fn) => {
101-
log.debug('Socket %s requesting to join Twitch chat channel "%s"', socket.id, channel);
102-
client.resetHeartbeat(channel);
103-
104-
// NOTE 2/1/2017: Rooms are only left when the socket itself is closed. Is this okay? Is this a leak?
105-
// Have the socket join the namespace for the channel in order to receive messages.
106-
const roomName = `channel:${channel}`;
107-
if (Object.keys(socket.rooms).indexOf(roomName) < 0) {
108-
log.trace('Socket %s joined room:', socket.id, roomName);
109-
socket.join(roomName);
110-
}
111-
112-
if (chatClient.channels.indexOf(`#${channel}`) >= 0) {
113-
// Already in channel, invoke callback with the name
114-
fn(null, channel);
115-
} else {
116-
chatClient.join(channel).then(() => {
117-
fn(null, null);
118-
}).catch(error => {
119-
log.error(`Error attempting to join "${channel}" from join command.\n\t`, error);
120-
fn(error);
121-
});
122-
}
123-
});
124-
125-
/**
126-
* Send a message to a Twitch chat channel as the user specified in the config file.
127-
* @param {String} channel - The name of the channel to send a message to. Do not include a leading "#" character.
128-
* @param {String} message - The message to send.
129-
* @param {Function} fn - The callback to execute after successfully sending the message.
130-
*/
131-
socket.on('say', (channel, message, fn) => {
132-
chatClient.say(channel, message).then(() => {
133-
fn(null, null);
134-
}).catch(error => {
135-
log.error(`Error attempting to "say" in channel "${channel}".\n\tMessage: ${message}\n\t`, error);
136-
fn(error);
137-
});
138-
});
139-
140-
/**
141-
* Timeout a user in a Twitch chat channel for a given number of seconds.
142-
* @param {String} channel - The name of the channel to execute the timeout command in.
143-
* Do not include a leading "#" character.
144-
* @param {String} username - The name of the user to timeout.
145-
* @param {Number} seconds - The number of seconds to time the user out for.
146-
* @param {Function} fn - The callback to execute after successfully timing out the user.
147-
*/
148-
socket.on('timeout', (channel, username, seconds, fn) => {
149-
chatClient.timeout(channel, username, seconds).then(() => {
150-
fn(null, null);
151-
}).catch(error => {
152-
log.error(`Error attempting to timeout user "${username}" in channel "${channel}" for ${seconds} seconds.\n\t`, error);
153-
fn(error);
154-
});
155-
});
156-
157-
/**
158-
* Get the list of chat mods for a Twitch channel.
159-
* @param {String} channel - The Twitch channel to get a list of chat mods from
160-
* @param {Function} fn - The callback to execute after successfully obtaining the list of chat mods.
161-
*/
162-
socket.on('mods', (channel, fn) => {
163-
chatClient.mods(channel).then(mods => {
164-
fn(null, mods);
165-
}).catch(error => {
166-
log.error(`Error attempting to get list of mods in channel "${channel}".\n\t`, error);
167-
fn(error);
168-
});
169-
});
170-
171-
/**
172-
* Tell Streen that you wish for it to remain in this array of channels.
173-
* @param {Array.<string>} channels - The array of channel names. Do not include leading "#" characters.
174-
* @param {heartbeatCallback} fb - The callback to execute after the heartbeat has been registered.
175-
*/
176-
socket.on('heartbeat', (channels, fn) => {
177-
client.heartbeat(channels);
178-
fn(null, HEARTBEAT_TIMEOUT);
179-
});
46+
const twitchClientConnected = client.connect();
47+
const delayPromise = new Promise(resolve => {
48+
setTimeout(() => resolve(), 1000);
49+
});
18050

181-
/**
182-
* The type of callback to execute after a successful heartbeat request.
183-
* @callback heartbeatCallback
184-
* @param {Object} err - The error returned, if any.
185-
* @param {Number} heartbeatTimeout - How long to wait (in milliseconds) before sending the next heartbeat.
186-
* Heartbeats can be sent earlier or later if needed.
187-
* A siphon has up to (heartbeatTimeout * 2 + 1000) milliseconds to
188-
* send another heartbeat before it times out. In other words, it can only miss
189-
* one consecutive heartbeat.
190-
*/
191-
}
51+
// Start the socket server after either connected to twitch, or after one second.
52+
// Prevents race conditions on server restart.
53+
Promise.race([twitchClientConnected, delayPromise]).then(
54+
() => setupServer(client),
55+
() => setupServer(client)
56+
);

0 commit comments

Comments
 (0)