Extending our Node.js and Redis chat server

Published by at 2nd March 2015 11:03 pm

In this tutorial, we're going to extend the chat system we built in the first tutorial to include the following functionality:

  • Persisting the data
  • Prompting users to sign in and storing their details in a Redis-backed session

In the process, we'll pick up a bit more about using Redis.

Persistence

Our first task is to make our messages persist when the session ends. Now, in order to do this, we're going to use a list. A list in Redis can be thought of as equivalent to an array or list in most programming languages, and can be retrieved by passing the key in a similar fashion to how you would retrieve a string.

As usual, we will write our test first. Open up test/test.js and replace the test for sending a message with this:

1 // Test sending a message
2 describe('Test sending a message', function () {
3 it("should return 'Message received'", function (done) {
4 // Connect to server
5 var socket = io.connect('http://localhost:5000', {
6 'reconnection delay' : 0,
7 'reopen delay' : 0,
8 'force new connection' : true
9 });
10
11 // Handle the message being received
12 socket.on('message', function (data) {
13 expect(data).to.include('Message received');
14
15 client.lrange('chat:messages', 0, -1, function (err, messages) {
16 // Check message has been persisted
17 var message_list = [];
18 messages.forEach(function (message, i) {
19 message_list.push(message);
20 });
21 expect(message_list[0]).to.include('Message received');
22
23 // Finish up
24 socket.disconnect();
25 done();
26 });
27 });
28
29 // Send the message
30 socket.emit('send', { message: 'Message received' });
31 });
32 });

The main difference here is that we use our Redis client to get the list chat:messages, and check to see if our message appears in it. Now, let's run our test to ensure it fails:

1$ npm test
2
3> babblr@1.0.0 test /Users/matthewdaly/Projects/babblr
4> grunt test --verbose
5
6Initializing
7Command-line options: --verbose
8
9Reading "Gruntfile.js" Gruntfile...OK
10
11Registering Gruntfile tasks.
12Initializing config...OK
13
14Registering "grunt-contrib-jshint" local Npm module tasks.
15Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
16Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
17Loading "jshint.js" tasks...OK
18+ jshint
19
20Registering "grunt-coveralls" local Npm module tasks.
21Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
22Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
23Loading "coverallsTask.js" tasks...OK
24+ coveralls
25
26Registering "grunt-mocha-istanbul" local Npm module tasks.
27Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
28Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
29Loading "index.js" tasks...OK
30+ istanbul_check_coverage, mocha_istanbul
31Loading "Gruntfile.js" tasks...OK
32+ test
33
34Running tasks: test
35
36Running "test" task
37
38Running "jshint" task
39
40Running "jshint:all" (jshint) task
41Verifying property jshint.all exists in config...OK
42Files: test/test.js, index.js -> all
43Options: force=false, reporterOutput=null
44OK
45>> 2 files lint free.
46
47Running "mocha_istanbul:coverage" (mocha_istanbul) task
48Verifying property mocha_istanbul.coverage exists in config...OK
49Files: test
50Options: require=[], ui=false, globals=[], reporter=false, timeout=false, coverage=false, slow=false, grep=false, dryRun=false, quiet=false, recursive=false, mask="*.js", root=false, print=false, noColors=false, harmony=false, coverageFolder="coverage", reportFormats=["cobertura","html","lcovonly"], check={"statements":false,"lines":false,"functions":false,"branches":false}, excludes=false, mochaOptions=false, istanbulOptions=false
51>> Will execute: node /Users/matthewdaly/Projects/babblr/node_modules/istanbul/lib/cli.js cover --dir=/Users/matthewdaly/Projects/babblr/coverage --report=cobertura --report=html --report=lcovonly /Users/matthewdaly/Projects/babblr/node_modules/mocha/bin/_mocha -- test/*.js
52Listening on port 5000
53
54
55 server
56Starting the server
57 Test the index route
58 ✓ should return a page with the title Babblr (484ms)
59 Test sending a message
60 1) should return 'Message received'
61Stopping the server
62
63
64 1 passing (552ms)
65 1 failing
66
67 1) server Test sending a message should return 'Message received':
68 Uncaught AssertionError: expected undefined to include 'Message received'
69 at /Users/matthewdaly/Projects/babblr/test/test.js:62:48
70 at try_callback (/Users/matthewdaly/Projects/babblr/node_modules/redis/index.js:592:9)
71 at RedisClient.return_reply (/Users/matthewdaly/Projects/babblr/node_modules/redis/index.js:685:13)
72 at HiredisReplyParser.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/redis/index.js:321:14)
73 at HiredisReplyParser.emit (events.js:95:17)
74 at HiredisReplyParser.execute (/Users/matthewdaly/Projects/babblr/node_modules/redis/lib/parser/hiredis.js:43:18)
75 at RedisClient.on_data (/Users/matthewdaly/Projects/babblr/node_modules/redis/index.js:547:27)
76 at Socket.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/redis/index.js:102:14)
77 at Socket.emit (events.js:95:17)
78 at Socket.<anonymous> (_stream_readable.js:765:14)
79 at Socket.emit (events.js:92:17)
80 at emitReadable_ (_stream_readable.js:427:10)
81 at emitReadable (_stream_readable.js:423:5)
82 at readableAddChunk (_stream_readable.js:166:9)
83 at Socket.Readable.push (_stream_readable.js:128:10)
84 at TCP.onread (net.js:529:21)
85
86
87
88=============================================================================
89Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
90Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
91=============================================================================
92
93=============================== Coverage summary ===============================
94Statements : 96.97% ( 32/33 ), 5 ignored
95Branches : 100% ( 6/6 ), 1 ignored
96Functions : 80% ( 4/5 )
97Lines : 96.97% ( 32/33 )
98================================================================================
99>>
100Warning: Task "mocha_istanbul:coverage" failed. Use --force to continue.
101
102Aborted due to warnings.
103npm ERR! Test failed. See above for more details.
104npm ERR! not ok code 0

Our test fails, so now we can start work on implementing the functionality we need. First of all, when a new message is sent, we need to push it to the list. Amend the new message handler in index.js to look like this:

1// Handle new messages
2io.sockets.on('connection', function (socket) {
3 // Subscribe to the Redis channel
4 subscribe.subscribe('ChatChannel');
5
6 // Handle incoming messages
7 socket.on('send', function (data) {
8 // Publish it
9 client.publish('ChatChannel', data.message);
10
11 // Persist it to a Redis list
12 client.rpush('chat:messages', 'Anonymous Coward : ' + data.message);
13 });
14
15 // Handle receiving messages
16 var callback = function (channel, data) {
17 socket.emit('message', data);
18 };
19 subscribe.on('message', callback);
20
21 // Handle disconnect
22 socket.on('disconnect', function () {
23 subscribe.removeListener('message', callback);
24 });
25});

The only significant change is the Persist it to a Redis list section. Here we call the RPUSH command to push the current message to chat:messages. RPUSH pushes a message to the end of the list. There's a similar command, LPUSH, which pushes an item to the beginning of the list, as well as LPOP and RPOP, which remove and return an item from the beginning and end of the list respectively.

Next we need to handle displaying the list when the main route loads. Replace the index route in index.js with this:

1// Define index route
2app.get('/', function (req, res) {
3 // Get messages
4 client.lrange('chat:messages', 0, -1, function (err, messages) {
5 /* istanbul ignore if */
6 if (err) {
7 console.log(err);
8 } else {
9 // Get messages
10 var message_list = [];
11 messages.forEach(function (message, i) {
12 /* istanbul ignore next */
13 message_list.push(message);
14 });
15
16 // Render page
17 res.render('index', { messages: message_list});
18 }
19 });
20});

Here we use the client to return all messages in the list by using the LRANGE command and defining the slice as being from the start to the end of the list. We then loop through the messages and push each to a list, before passing that list to the view.

Speaking of which, we also need to update views/index.hbs:

1___HANDLEBARS0___
2 <div class="container">
3 <div class="row">
4 <div class="col-md-8">
5 <div class="conversation">
6 ___HANDLEBARS1___
7 <p>___HANDLEBARS2___</p>
8 ___HANDLEBARS3___
9 </div>
10 </div>
11 <div class="col-md-4">
12 <form>
13 <div class="form-group">
14 <label for="message">Message</label>
15 <textarea class="form-control" id="message" rows="20"></textarea>
16 <a id="submitbutton" class="btn btn-primary form-control">Submit</a>
17 <div>
18 </form>
19 </div>
20 </div>
21 </div>
22___HANDLEBARS4___

This just loops through the messages and prints each one in turn. Now let's run our tests and make sure they pass:

1$ npm test
2
3> babblr@1.0.0 test /Users/matthewdaly/Projects/babblr
4> grunt test --verbose
5
6Initializing
7Command-line options: --verbose
8
9Reading "Gruntfile.js" Gruntfile...OK
10
11Registering Gruntfile tasks.
12Initializing config...OK
13
14Registering "grunt-contrib-jshint" local Npm module tasks.
15Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
16Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
17Loading "jshint.js" tasks...OK
18+ jshint
19
20Registering "grunt-coveralls" local Npm module tasks.
21Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
22Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
23Loading "coverallsTask.js" tasks...OK
24+ coveralls
25
26Registering "grunt-mocha-istanbul" local Npm module tasks.
27Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
28Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
29Loading "index.js" tasks...OK
30+ istanbul_check_coverage, mocha_istanbul
31Loading "Gruntfile.js" tasks...OK
32+ test
33
34Running tasks: test
35
36Running "test" task
37
38Running "jshint" task
39
40Running "jshint:all" (jshint) task
41Verifying property jshint.all exists in config...OK
42Files: test/test.js, index.js -> all
43Options: force=false, reporterOutput=null
44OK
45>> 2 files lint free.
46
47Running "mocha_istanbul:coverage" (mocha_istanbul) task
48Verifying property mocha_istanbul.coverage exists in config...OK
49Files: test
50Options: require=[], ui=false, globals=[], reporter=false, timeout=false, coverage=false, slow=false, grep=false, dryRun=false, quiet=false, recursive=false, mask="*.js", root=false, print=false, noColors=false, harmony=false, coverageFolder="coverage", reportFormats=["cobertura","html","lcovonly"], check={"statements":false,"lines":false,"functions":false,"branches":false}, excludes=false, mochaOptions=false, istanbulOptions=false
51>> Will execute: node /Users/matthewdaly/Projects/babblr/node_modules/istanbul/lib/cli.js cover --dir=/Users/matthewdaly/Projects/babblr/coverage --report=cobertura --report=html --report=lcovonly /Users/matthewdaly/Projects/babblr/node_modules/mocha/bin/_mocha -- test/*.js
52Listening on port 5000
53
54
55 server
56Starting the server
57 Test the index route
58 ✓ should return a page with the title Babblr (1262ms)
59 Test sending a message
60 ✓ should return 'Message received' (48ms)
61Stopping the server
62
63
64 2 passing (2s)
65
66=============================================================================
67Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
68Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
69=============================================================================
70
71=============================== Coverage summary ===============================
72Statements : 100% ( 40/40 ), 7 ignored
73Branches : 100% ( 8/8 ), 2 ignored
74Functions : 85.71% ( 6/7 )
75Lines : 100% ( 40/40 )
76================================================================================
77>> Done. Check coverage folder.
78
79Running "coveralls" task
80
81Running "coveralls:app" (coveralls) task
82Verifying property coveralls.app exists in config...OK
83Files: coverage/lcov.info
84Options: src="coverage/lcov.info", force=false
85Submitting file to coveralls.io: coverage/lcov.info
86>> Failed to submit 'coverage/lcov.info' to coveralls: Bad response: 422 {"message":"Couldn't find a repository matching this job.","error":true}
87>> Failed to submit coverage results to coveralls
88Warning: Task "coveralls:app" failed. Use --force to continue.
89
90Aborted due to warnings.
91npm ERR! Test failed. See above for more details.
92npm ERR! not ok code 0

As before, don't worry about Coveralls not working - it's only an issue when it runs on Travis CI. If everything else is fine, our chat server should now persist our changes.

Sessions and user login

At present, it's hard to carry on a conversation with someone using this site because you can't see who is responding to you. We need to implement a mechanism to obtain a username for each user, store it in a session, and then use it to identify all of a user's messages. In this case, we're going to just prompt the user to enter a username of their choice, but if you wish, you can use something like Passport.js to allow authentication using third-party services - I'll leave that as an exercise for the reader.

Now, Express doesn't include any support for sessions out of the box, so we have to install some additional libraries:

$ npm install connect-redis express-session body-parser --save

The express-session library is middleware for Express that allows for storing and retrieving session variables, while connect-redis allows it to use Redis to store this data. We used body-parser for the URL shortener to process POST data, so we will use it again here. Now, we need to set it up. Replace the part of index.js before we set up the templating with this:

1/*jslint node: true */
2'use strict';
3
4// Declare variables used
5var app, base_url, client, express, hbs, io, port, RedisStore, rtg, session, subscribe;
6
7// Define values
8express = require('express');
9app = express();
10port = process.env.PORT || 5000;
11base_url = process.env.BASE_URL || 'http://localhost:5000';
12hbs = require('hbs');
13session = require('express-session');
14RedisStore = require('connect-redis')(session);
15
16// Set up connection to Redis
17/* istanbul ignore if */
18if (process.env.REDISTOGO_URL) {
19 rtg = require('url').parse(process.env.REDISTOGO_URL);
20 client = require('redis').createClient(rtg.port, rtg.hostname);
21 subscribe = require('redis').createClient(rtg.port, rtg.hostname);
22 client.auth(rtg.auth.split(':')[1]);
23 subscribe.auth(rtg.auth.split(':')[1]);
24} else {
25 client = require('redis').createClient();
26 subscribe = require('redis').createClient();
27}
28
29// Set up session
30app.use(session({
31 store: new RedisStore({
32 client: client
33 }),
34 secret: 'blibble'
35}));

This just sets up the session and configures it to use Redis as the back end. Don't forget to change the value of secret.

Now, let's plan out how our username system is going to work. If a user visits the site and there is no session set, then they should be redirected to a new route, /login. Here they will be prompted to enter a username. Once a satisfactory username (eg one or more characters) has been submitted via the form, it should be stored in the session and the user redirected to the index. There should also be a /logout route to destroy the session and redirect the user back to the login form.

First, let's implement a test for fetching the login form in test/test.js:

1 // Test submitting to the login route
2 describe('Test submitting to the login route', function () {
3 it('should store the username in the session and redirect the user to the index', function (done) {
4 request.post({ url: 'http://localhost:5000/login',
5 form:{username: 'bobsmith'},
6 followRedirect: false},
7 function (error, response, body) {
8 expect(response.headers.location).to.equal('/');
9 expect(response.statusCode).to.equal(302);
10 done();
11 });
12 });
13 });

This test sends a POST request containing the field username with the value bobsmith. We expect to be redirected to the index route.

Let's run the test to make sure it fails:

1$ npm test
2
3> babblr@1.0.0 test /Users/matthewdaly/Projects/babblr
4> grunt test --verbose
5
6Initializing
7Command-line options: --verbose
8
9Reading "Gruntfile.js" Gruntfile...OK
10
11Registering Gruntfile tasks.
12Initializing config...OK
13
14Registering "grunt-contrib-jshint" local Npm module tasks.
15Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
16Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
17Loading "jshint.js" tasks...OK
18+ jshint
19
20Registering "grunt-coveralls" local Npm module tasks.
21Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
22Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
23Loading "coverallsTask.js" tasks...OK
24+ coveralls
25
26Registering "grunt-mocha-istanbul" local Npm module tasks.
27Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
28Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
29Loading "index.js" tasks...OK
30+ istanbul_check_coverage, mocha_istanbul
31Loading "Gruntfile.js" tasks...OK
32+ test
33
34Running tasks: test
35
36Running "test" task
37
38Running "jshint" task
39
40Running "jshint:all" (jshint) task
41Verifying property jshint.all exists in config...OK
42Files: test/test.js, index.js -> all
43Options: force=false, reporterOutput=null
44OK
45>> 2 files lint free.
46
47Running "mocha_istanbul:coverage" (mocha_istanbul) task
48Verifying property mocha_istanbul.coverage exists in config...OK
49Files: test
50Options: require=[], ui=false, globals=[], reporter=false, timeout=false, coverage=false, slow=false, grep=false, dryRun=false, quiet=false, recursive=false, mask="*.js", root=false, print=false, noColors=false, harmony=false, coverageFolder="coverage", reportFormats=["cobertura","html","lcovonly"], check={"statements":false,"lines":false,"functions":false,"branches":false}, excludes=false, mochaOptions=false, istanbulOptions=false
51>> Will execute: node /Users/matthewdaly/Projects/babblr/node_modules/istanbul/lib/cli.js cover --dir=/Users/matthewdaly/Projects/babblr/coverage --report=cobertura --report=html --report=lcovonly /Users/matthewdaly/Projects/babblr/node_modules/mocha/bin/_mocha -- test/*.js
52express-session deprecated undefined resave option; provide resave option index.js:9:1585
53express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:9:1585
54Listening on port 5000
55
56
57 server
58Starting the server
59 Test the index route
60 ✓ should return a page with the title Babblr (45ms)
61 Test the login route
62 ✓ should return a page with the text Please enter a handle
63 Test submitting to the login route
64 1) should store the username in the session and redirect the user to the index
65 Test sending a message
66 ✓ should return 'Message received' (42ms)
67Stopping the server
68
69
70 3 passing (122ms)
71 1 failing
72
73 1) server Test submitting to the login route should store the username in the session and redirect the user to the index:
74 Uncaught AssertionError: expected undefined to equal '/'
75 at Request._callback (/Users/matthewdaly/Projects/babblr/test/test.js:61:58)
76 at Request.self.callback (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:373:22)
77 at Request.emit (events.js:98:17)
78 at Request.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:1318:14)
79 at Request.emit (events.js:117:20)
80 at IncomingMessage.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:1266:12)
81 at IncomingMessage.emit (events.js:117:20)
82 at _stream_readable.js:944:16
83 at process._tickCallback (node.js:442:13)
84
85
86
87=============================================================================
88Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
89Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
90=============================================================================
91
92=============================== Coverage summary ===============================
93Statements : 100% ( 45/45 ), 7 ignored
94Branches : 100% ( 8/8 ), 2 ignored
95Functions : 87.5% ( 7/8 )
96Lines : 100% ( 45/45 )
97================================================================================
98>>
99Warning: Task "mocha_istanbul:coverage" failed. Use --force to continue.
100
101Aborted due to warnings.
102npm ERR! Test failed. See above for more details.
103npm ERR! not ok code 0

Now, all we need to do to make this test pass is create a view containing the form and define a route to display it. First, we'll define our new route in index.js:

1// Define login route
2app.get('/login', function (req, res) {
3 // Render view
4 res.render('login');
5});

Next, we'll create our new template at views/login.hbs:

1___HANDLEBARS0___
2 <div class="container">
3 <div class="row">
4 <div class="col-md-12">
5 <form action="/login" method="POST">
6 <div class="form-group">
7 <label for="Username">Please enter a handle</label>
8 <input type="text" class="form-control" size="20" required id="username" name="username"></input>
9 <input type="submit" class="btn btn-primary form-control"></input>
10 <div>
11 </form>
12 </div>
13 </div>
14 </div>
15___HANDLEBARS1___

Let's run our tests and make sure they pass:

1$ npm test
2
3> babblr@1.0.0 test /Users/matthewdaly/Projects/babblr
4> grunt test --verbose
5
6Initializing
7Command-line options: --verbose
8
9Reading "Gruntfile.js" Gruntfile...OK
10
11Registering Gruntfile tasks.
12Initializing config...OK
13
14Registering "grunt-contrib-jshint" local Npm module tasks.
15Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
16Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
17Loading "jshint.js" tasks...OK
18+ jshint
19
20Registering "grunt-coveralls" local Npm module tasks.
21Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
22Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
23Loading "coverallsTask.js" tasks...OK
24+ coveralls
25
26Registering "grunt-mocha-istanbul" local Npm module tasks.
27Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
28Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
29Loading "index.js" tasks...OK
30+ istanbul_check_coverage, mocha_istanbul
31Loading "Gruntfile.js" tasks...OK
32+ test
33
34Running tasks: test
35
36Running "test" task
37
38Running "jshint" task
39
40Running "jshint:all" (jshint) task
41Verifying property jshint.all exists in config...OK
42Files: test/test.js, index.js -> all
43Options: force=false, reporterOutput=null
44OK
45>> 2 files lint free.
46
47Running "mocha_istanbul:coverage" (mocha_istanbul) task
48Verifying property mocha_istanbul.coverage exists in config...OK
49Files: test
50Options: require=[], ui=false, globals=[], reporter=false, timeout=false, coverage=false, slow=false, grep=false, dryRun=false, quiet=false, recursive=false, mask="*.js", root=false, print=false, noColors=false, harmony=false, coverageFolder="coverage", reportFormats=["cobertura","html","lcovonly"], check={"statements":false,"lines":false,"functions":false,"branches":false}, excludes=false, mochaOptions=false, istanbulOptions=false
51>> Will execute: node /Users/matthewdaly/Projects/babblr/node_modules/istanbul/lib/cli.js cover --dir=/Users/matthewdaly/Projects/babblr/coverage --report=cobertura --report=html --report=lcovonly /Users/matthewdaly/Projects/babblr/node_modules/mocha/bin/_mocha -- test/*.js
52express-session deprecated undefined resave option; provide resave option index.js:9:1585
53express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:9:1585
54Listening on port 5000
55
56
57 server
58Starting the server
59 Test the index route
60 ✓ should return a page with the title Babblr (64ms)
61 Test the login route
62 ✓ should return a page with the text Please enter a handle
63 Test sending a message
64 ✓ should return 'Message received' (78ms)
65Stopping the server
66
67
68 3 passing (179ms)
69
70=============================================================================
71Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
72Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
73=============================================================================
74
75=============================== Coverage summary ===============================
76Statements : 100% ( 45/45 ), 7 ignored
77Branches : 100% ( 8/8 ), 2 ignored
78Functions : 87.5% ( 7/8 )
79Lines : 100% ( 45/45 )
80================================================================================
81>> Done. Check coverage folder.
82
83Running "coveralls" task
84
85Running "coveralls:app" (coveralls) task
86Verifying property coveralls.app exists in config...OK
87Files: coverage/lcov.info
88Options: src="coverage/lcov.info", force=false
89Submitting file to coveralls.io: coverage/lcov.info
90>> Failed to submit 'coverage/lcov.info' to coveralls: Bad response: 422 {"message":"Couldn't find a repository matching this job.","error":true}
91>> Failed to submit coverage results to coveralls
92Warning: Task "coveralls:app" failed. Use --force to continue.
93
94Aborted due to warnings.
95npm ERR! Test failed. See above for more details.
96npm ERR! not ok code 0

Next, we need to process the submitted form, set the session, and redirect the user back to the index. First, let's add another test:

1 // Test submitting to the login route
2 describe('Test submitting to the login route', function () {
3 it('should store the username in the session and redirect the user to the index', function (done) {
4 request.post({ url: 'http://localhost:5000/login',
5 form:{username: 'bobsmith'},
6 followRedirect: false},
7 function (error, response, body) {
8 expect(response.headers.location).to.equal('http://localhost:5000');
9 expect(response.statusCode).to.equal(301);
10 done();
11 });
12 });
13 });

This test submits the username, and makes sure that the response received is a 301 redirect to the index route. Let's check to make sure it fails:

1$ npm test
2
3> babblr@1.0.0 test /Users/matthewdaly/Projects/babblr
4> grunt test --verbose
5
6Initializing
7Command-line options: --verbose
8
9Reading "Gruntfile.js" Gruntfile...OK
10
11Registering Gruntfile tasks.
12Initializing config...OK
13
14Registering "grunt-contrib-jshint" local Npm module tasks.
15Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
16Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
17Loading "jshint.js" tasks...OK
18+ jshint
19
20Registering "grunt-coveralls" local Npm module tasks.
21Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
22Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
23Loading "coverallsTask.js" tasks...OK
24+ coveralls
25
26Registering "grunt-mocha-istanbul" local Npm module tasks.
27Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
28Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
29Loading "index.js" tasks...OK
30+ istanbul_check_coverage, mocha_istanbul
31Loading "Gruntfile.js" tasks...OK
32+ test
33
34Running tasks: test
35
36Running "test" task
37
38Running "jshint" task
39
40Running "jshint:all" (jshint) task
41Verifying property jshint.all exists in config...OK
42Files: test/test.js, index.js -> all
43Options: force=false, reporterOutput=null
44OK
45>> 2 files lint free.
46
47Running "mocha_istanbul:coverage" (mocha_istanbul) task
48Verifying property mocha_istanbul.coverage exists in config...OK
49Files: test
50Options: require=[], ui=false, globals=[], reporter=false, timeout=false, coverage=false, slow=false, grep=false, dryRun=false, quiet=false, recursive=false, mask="*.js", root=false, print=false, noColors=false, harmony=false, coverageFolder="coverage", reportFormats=["cobertura","html","lcovonly"], check={"statements":false,"lines":false,"functions":false,"branches":false}, excludes=false, mochaOptions=false, istanbulOptions=false
51>> Will execute: node /Users/matthewdaly/Projects/babblr/node_modules/istanbul/lib/cli.js cover --dir=/Users/matthewdaly/Projects/babblr/coverage --report=cobertura --report=html --report=lcovonly /Users/matthewdaly/Projects/babblr/node_modules/mocha/bin/_mocha -- test/*.js
52express-session deprecated undefined resave option; provide resave option index.js:9:1585
53express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:9:1585
54Listening on port 5000
55
56
57 server
58Starting the server
59 Test the index route
60 ✓ should return a page with the title Babblr (476ms)
61 Test the login route
62 ✓ should return a page with the text Please enter a handle
63 Test submitting to the login route
64 1) should store the username in the session and redirect the user to the index
65 Test sending a message
66 ✓ should return 'Message received' (42ms)
67Stopping the server
68
69
70 3 passing (557ms)
71 1 failing
72
73 1) server Test submitting to the login route should store the username in the session and redirect the user to the index:
74 Uncaught AssertionError: expected undefined to equal 'http://localhost:5000'
75 at Request._callback (/Users/matthewdaly/Projects/babblr/test/test.js:61:58)
76 at Request.self.callback (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:373:22)
77 at Request.emit (events.js:98:17)
78 at Request.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:1318:14)
79 at Request.emit (events.js:117:20)
80 at IncomingMessage.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:1266:12)
81 at IncomingMessage.emit (events.js:117:20)
82 at _stream_readable.js:944:16
83 at process._tickCallback (node.js:442:13)
84
85
86
87=============================================================================
88Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
89Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
90=============================================================================
91
92=============================== Coverage summary ===============================
93Statements : 100% ( 45/45 ), 7 ignored
94Branches : 100% ( 8/8 ), 2 ignored
95Functions : 87.5% ( 7/8 )
96Lines : 100% ( 45/45 )
97================================================================================
98>>
99Warning: Task "mocha_istanbul:coverage" failed. Use --force to continue.
100
101Aborted due to warnings.
102npm ERR! Test failed. See above for more details.
103npm ERR! not ok code 0

Now, in order to process POST data we'll need to use body-parser. Amend the top of index.js to look like this::

1/*jslint node: true */
2'use strict';
3
4// Declare variables used
5var app, base_url, bodyParser, client, express, hbs, io, port, RedisStore, rtg, session, subscribe;
6
7// Define values
8express = require('express');
9app = express();
10bodyParser = require('body-parser');
11port = process.env.PORT || 5000;
12base_url = process.env.BASE_URL || 'http://localhost:5000';
13hbs = require('hbs');
14session = require('express-session');
15RedisStore = require('connect-redis')(session);
16
17// Set up connection to Redis
18/* istanbul ignore if */
19if (process.env.REDISTOGO_URL) {
20 rtg = require('url').parse(process.env.REDISTOGO_URL);
21 client = require('redis').createClient(rtg.port, rtg.hostname);
22 subscribe = require('redis').createClient(rtg.port, rtg.hostname);
23 client.auth(rtg.auth.split(':')[1]);
24 subscribe.auth(rtg.auth.split(':')[1]);
25} else {
26 client = require('redis').createClient();
27 subscribe = require('redis').createClient();
28}
29
30// Set up session
31app.use(session({
32 store: new RedisStore({
33 client: client
34 }),
35 secret: 'blibble'
36}));
37
38// Set up templating
39app.set('views', __dirname + '/views');
40app.set('view engine', "hbs");
41app.engine('hbs', require('hbs').__express);
42
43// Register partials
44hbs.registerPartials(__dirname + '/views/partials');
45
46// Set URL
47app.set('base_url', base_url);
48
49// Handle POST data
50app.use(bodyParser.json());
51app.use(bodyParser.urlencoded({
52 extended: true
53}));

Next, we define a POST route to handle the username input:

1// Process login
2app.post('/login', function (req, res) {
3 // Get username
4 var username = req.body.username;
5
6 // If username length is zero, reload the page
7 if (username.length === 0) {
8 res.render('login');
9 } else {
10 // Store username in session and redirect to index
11 req.session.username = username;
12 res.redirect('/');
13 }
14});

This should be fairly straightforward. This route accepts a username parameter. If this parameter is not present, the user will see the login form again. Otherwise, they are redirected back to the index.

Now, if you check coverage/index.html after running the tests again, you'll notice that there's a gap in our coverage for the scenario when a user submits an empty username. Let's fix that - add the following test to test/test.js:

1 // Test empty login
2 describe('Test empty login', function () {
3 it('should show the login form', function (done) {
4 request.post({ url: 'http://localhost:5000/login',
5 form:{username: ''},
6 followRedirect: false},
7 function (error, response, body) {
8 expect(response.statusCode).to.equal(200);
9 expect(body).to.include('Please enter a handle');
10 done();
11 });
12 });
13 });

Let's run our tests again:

1$ npm test
2
3> babblr@1.0.0 test /Users/matthewdaly/Projects/babblr
4> grunt test --verbose
5
6Initializing
7Command-line options: --verbose
8
9Reading "Gruntfile.js" Gruntfile...OK
10
11Registering Gruntfile tasks.
12Initializing config...OK
13
14Registering "grunt-contrib-jshint" local Npm module tasks.
15Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
16Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-contrib-jshint/package.json...OK
17Loading "jshint.js" tasks...OK
18+ jshint
19
20Registering "grunt-coveralls" local Npm module tasks.
21Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
22Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-coveralls/package.json...OK
23Loading "coverallsTask.js" tasks...OK
24+ coveralls
25
26Registering "grunt-mocha-istanbul" local Npm module tasks.
27Reading /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
28Parsing /Users/matthewdaly/Projects/babblr/node_modules/grunt-mocha-istanbul/package.json...OK
29Loading "index.js" tasks...OK
30+ istanbul_check_coverage, mocha_istanbul
31Loading "Gruntfile.js" tasks...OK
32+ test
33
34Running tasks: test
35
36Running "test" task
37
38Running "jshint" task
39
40Running "jshint:all" (jshint) task
41Verifying property jshint.all exists in config...OK
42Files: test/test.js, index.js -> all
43Options: force=false, reporterOutput=null
44OK
45>> 2 files lint free.
46
47Running "mocha_istanbul:coverage" (mocha_istanbul) task
48Verifying property mocha_istanbul.coverage exists in config...OK
49Files: test
50Options: require=[], ui=false, globals=[], reporter=false, timeout=false, coverage=false, slow=false, grep=false, dryRun=false, quiet=false, recursive=false, mask="*.js", root=false, print=false, noColors=false, harmony=false, coverageFolder="coverage", reportFormats=["cobertura","html","lcovonly"], check={"statements":false,"lines":false,"functions":false,"branches":false}, excludes=false, mochaOptions=false, istanbulOptions=false
51>> Will execute: node /Users/matthewdaly/Projects/babblr/node_modules/istanbul/lib/cli.js cover --dir=/Users/matthewdaly/Projects/babblr/coverage --report=cobertura --report=html --report=lcovonly /Users/matthewdaly/Projects/babblr/node_modules/mocha/bin/_mocha -- test/*.js
52express-session deprecated undefined resave option; provide resave option index.js:9:1669
53express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:9:1669
54Listening on port 5000
55
56
57 server
58Starting the server
59 Test the index route
60 ✓ should return a page with the title Babblr (44ms)
61 Test the login route
62 ✓ should return a page with the text Please enter a handle
63 Test submitting to the login route
64 ✓ should store the username in the session and redirect the user to the index
65 Test empty login
66 ✓ should show the login form
67 Test sending a message
68 ✓ should return 'Message received' (41ms)
69Stopping the server
70
71
72 5 passing (145ms)
73
74=============================================================================
75Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
76Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
77=============================================================================
78
79=============================== Coverage summary ===============================
80Statements : 100% ( 54/54 ), 7 ignored
81Branches : 100% ( 10/10 ), 2 ignored
82Functions : 88.89% ( 8/9 )
83Lines : 100% ( 54/54 )
84================================================================================
85>> Done. Check coverage folder.
86
87Running "coveralls" task
88
89Running "coveralls:app" (coveralls) task
90Verifying property coveralls.app exists in config...OK
91Files: coverage/lcov.info
92Options: src="coverage/lcov.info", force=false
93Submitting file to coveralls.io: coverage/lcov.info
94>> Failed to submit 'coverage/lcov.info' to coveralls: Bad response: 422 {"message":"Couldn't find a repository matching this job.","error":true}
95>> Failed to submit coverage results to coveralls
96Warning: Task "coveralls:app" failed. Use --force to continue.
97
98Aborted due to warnings.
99npm ERR! Test failed. See above for more details.
100npm ERR! not ok code 0

Our test now passes (bar, of course, Coveralls failing). Our next step is to actually do something with the session. Now, the request module we use in our test requires a third-party module called tough-cookie to work with cookies, so we need to install that:

$ npm install tough-cookie --save-dev

Next, amend the login test as follows:

1 // Test submitting to the login route
2 describe('Test submitting to the login route', function () {
3 it('should store the username in the session and redirect the user to the index', function (done) {
4 request.post({ url: 'http://localhost:5000/login',
5 form:{username: 'bobsmith'},
6 jar: true,
7 followRedirect: false},
8 function (error, response, body) {
9 expect(response.headers.location).to.equal('/');
10 expect(response.statusCode).to.equal(302);
11
12 // Check the username
13 request.get({ url: 'http://localhost:5000/', jar: true }, function (error, response, body) {
14 expect(body).to.include('bobsmith');
15 done();
16 });
17 });
18 });
19 });

Here we're using a new parameter, namely jar - this tells request to store the cookies. We POST the username to the login form, and then we get the index route and verify that the username is shown in the request. Check the test fails, then amend the index route in index.js as follows:

1// Define index route
2app.get('/', function (req, res) {
3 // Get messages
4 client.lrange('chat:messages', 0, -1, function (err, messages) {
5 /* istanbul ignore if */
6 if (err) {
7 console.log(err);
8 } else {
9 // Get username
10 var username = req.session.username;
11
12 // Get messages
13 var message_list = [];
14 messages.forEach(function (message, i) {
15 /* istanbul ignore next */
16 message_list.push(message);
17 });
18
19 // Render page
20 res.render('index', { messages: message_list, username: username });
21 }
22 });
23});

Note we get the username and pass it through to the view. We need to adapt the header view to display the username. Amend views/partials/header.hbs to look like this:

1<!DOCTYPE html>
2<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
3<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
4<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
5<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
6 <head>
7 <meta charset="utf-8">
8 <meta http-equiv="X-UA-Compatible" content="IE=edge">
9 <title>Babblr</title>
10 <meta name="description" content="">
11 <meta name="viewport" content="width=device-width, initial-scale=1">
12
13 <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
14
15 <link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
16 <link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap-theme.min.css">
17 <link rel="stylesheet" href="/css/style.css">
18 </head>
19 <body>
20 <!--[if lt IE 7]>
21 <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
22 <![endif]-->
23 <nav class="navbar navbar-inverse navbar-static-top" role="navigation">
24 <div class="container-fluid">
25 <div class="navbar-header">
26 <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-nav">
27 <span class="icon-bar"></span>
28 <span class="icon-bar"></span>
29 <span class="icon-bar"></span>
30 </button>
31 <a class="navbar-brand" href="/">Babblr</a>
32 <div class="collapse navbar-collapse navbar-right" id="header-nav">
33 <ul class="nav navbar-nav">
34 ___HANDLEBARS0___
35 <li><a href="/logout">Logged in as ___HANDLEBARS1___</a></li>
36 ___HANDLEBARS2___
37 <li><a href="/login">Log in</a></li>
38 ___HANDLEBARS3___
39 </ul>
40 </div>
41 </div>
42 </div>
43 </nav>

Note the addition of a logout link, which we will implement later. Let's check our tests pass:

1$ grunt test
2Running "jshint:all" (jshint) task
3>> 2 files lint free.
4
5Running "mocha_istanbul:coverage" (mocha_istanbul) task
6express-session deprecated undefined resave option; provide resave option index.js:9:1669
7express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:9:1669
8Listening on port 5000
9
10
11 server
12Starting the server
13 Test the index route
14 ✓ should return a page with the title Babblr (44ms)
15 Test the login route
16 ✓ should return a page with the text Please enter a handle
17 Test submitting to the login route
18 ✓ should store the username in the session and redirect the user to the index
19 Test empty login
20 ✓ should show the login form
21 Test sending a message
22 ✓ should return 'Message received' (45ms)
23Stopping the server
24
25
26 5 passing (156ms)
27
28=============================================================================
29Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
30Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
31=============================================================================
32
33=============================== Coverage summary ===============================
34Statements : 100% ( 55/55 ), 7 ignored
35Branches : 100% ( 10/10 ), 2 ignored
36Functions : 88.89% ( 8/9 )
37Lines : 100% ( 55/55 )
38================================================================================
39>> Done. Check coverage folder.
40
41Running "coveralls:app" (coveralls) task
42>> Failed to submit 'coverage/lcov.info' to coveralls: Bad response: 422 {"message":"Couldn't find a repository matching this job.","error":true}
43>> Failed to submit coverage results to coveralls
44Warning: Task "coveralls:app" failed. Use --force to continue.
45
46Aborted due to warnings.

Excellent! Next, let's implement the test for our logout route:

1 // Test logout
2 describe('Test logout', function () {
3 it('should log the user out', function (done) {
4 request.post({ url: 'http://localhost:5000/login',
5 form:{username: 'bobsmith'},
6 jar: true,
7 followRedirect: false},
8 function (error, response, body) {
9 expect(response.headers.location).to.equal('/');
10 expect(response.statusCode).to.equal(302);
11
12 // Check the username
13 request.get({ url: 'http://localhost:5000/', jar: true }, function (error, response, body) {
14 expect(body).to.include('bobsmith');
15
16 // Log the user out
17 request.get({ url: 'http://localhost:5000/logout', jar: true }, function (error, response, body) {
18 expect(body).to.include('Log in');
19 done();
20 });
21 });
22 });
23 });
24 });

This is largely the same as the previous test, but adds some additional content at the end to test logging out afterwards. Let's run the test:

1$ grunt test
2Running "jshint:all" (jshint) task
3>> 2 files lint free.
4
5Running "mocha_istanbul:coverage" (mocha_istanbul) task
6express-session deprecated undefined resave option; provide resave option index.js:9:1669
7express-session deprecated undefined saveUninitialized option; provide saveUninitialized option index.js:9:1669
8Listening on port 5000
9
10
11 server
12Starting the server
13 Test the index route
14 ✓ should return a page with the title Babblr (536ms)
15 Test the login route
16 ✓ should return a page with the text Please enter a handle
17 Test submitting to the login route
18 ✓ should store the username in the session and redirect the user to the index
19 Test empty login
20 ✓ should show the login form
21 Test logout
22 1) should log the user out
23 Test sending a message
24 ✓ should return 'Message received' (49ms)
25Stopping the server
26
27
28 5 passing (682ms)
29 1 failing
30
31 1) server Test logout should log the user out:
32 Uncaught AssertionError: expected 'Cannot GET /logout\n' to include 'Log in'
33 at Request._callback (/Users/matthewdaly/Projects/babblr/test/test.js:105:45)
34 at Request.self.callback (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:373:22)
35 at Request.emit (events.js:98:17)
36 at Request.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:1318:14)
37 at Request.emit (events.js:117:20)
38 at IncomingMessage.<anonymous> (/Users/matthewdaly/Projects/babblr/node_modules/request/request.js:1266:12)
39 at IncomingMessage.emit (events.js:117:20)
40 at _stream_readable.js:944:16
41 at process._tickCallback (node.js:442:13)
42
43
44
45=============================================================================
46Writing coverage object [/Users/matthewdaly/Projects/babblr/coverage/coverage.json]
47Writing coverage reports at [/Users/matthewdaly/Projects/babblr/coverage]
48=============================================================================
49
50=============================== Coverage summary ===============================
51Statements : 100% ( 55/55 ), 7 ignored
52Branches : 100% ( 10/10 ), 2 ignored
53Functions : 88.89% ( 8/9 )
54Lines : 100% ( 55/55 )
55================================================================================
56>>
57Warning: Task "mocha_istanbul:coverage" failed. Use --force to continue.
58
59Aborted due to warnings.

Now we have a failing test, let's implement our logout route. Add the following route to index.js:

1// Process logout
2app.get('/logout', function (req, res) {
3 // Delete username from session
4 req.session.username = null;
5
6 // Redirect user
7 res.redirect('/');
8});

If you run your tests again, they should now pass.

Now that we have the user's name stored in the session, we can make use of it. First, let's amend static/js/main.js so that it no longer adds a default username:

1$(document).ready(function () {
2 'use strict';
3
4 // Set up the connection
5 var field, socket, output;
6 socket = io.connect(window.location.href);
7
8 // Get a reference to the input
9 field = $('textarea#message');
10
11 // Get a reference to the output
12 output = $('div.conversation');
13
14 // Handle message submit
15 $('a#submitbutton').on('click', function () {
16 // Create the message
17 var msg;
18 msg = field.val();
19 socket.emit('send', { message: msg });
20 field.val('');
21 });
22
23 // Handle incoming messages
24 socket.on('message', function (data) {
25 // Insert the message
26 output.append('<p>' + data + '</p>');
27 });
28});

Then, in index.js, we need to declare a variable for our session middleware, which will be shared between Socket.IO and Express:

1// Declare variables used
2var app, base_url, bodyParser, client, express, hbs, io, port, RedisStore, rtg, session, sessionMiddleware, subscribe;

Then we amend the session setup to make it easier to reuse for Socket.IO:

1// Set up session
2sessionMiddleware = session({
3 store: new RedisStore({
4 client: client
5 }),
6 secret: 'blibble'
7});
8app.use(sessionMiddleware);

Towards the end of the file, before we set up our handlers for Socket.IO, we integrate our sessions:

1// Integrate sessions
2io.use(function(socket, next) {
3 sessionMiddleware(socket.request, socket.request.res, next);
4});

Finally, we rewrite our session handlers to use the username from the session:

1// Handle new messages
2io.sockets.on('connection', function (socket) {
3 // Subscribe to the Redis channel
4 subscribe.subscribe('ChatChannel');
5
6 // Handle incoming messages
7 socket.on('send', function (data) {
8 // Define variables
9 var username, message;
10
11 // Get username
12 username = socket.request.session.username;
13 if (!username) {
14 username = 'Anonymous Coward';
15 }
16 message = username + ': ' + data.message;
17
18 // Publish it
19 client.publish('ChatChannel', message);
20
21 // Persist it to a Redis list
22 client.rpush('chat:messages', message);
23 });
24
25 // Handle receiving messages
26 var callback = function (channel, data) {
27 socket.emit('message', data);
28 };
29 subscribe.on('message', callback);
30
31 // Handle disconnect
32 socket.on('disconnect', function () {
33 subscribe.removeListener('message', callback);
34 });
35});

Note here that when a message is sent, we get the username from the session, and if it's empty, set it to Anonymous Coward. We then prepend it to the message, publish it, and persist it.

One final thing...

One last job remains. At present, users can pass JavaScript through in messages, which is not terribly secure! We need to fix it. Amend the send handler as follows:

1 // Handle incoming messages
2 socket.on('send', function (data) {
3 // Define variables
4 var username, message;
5
6 // Strip tags from message
7 message = data.message.replace(/<[^>]*>/g, '');
8
9 // Get username
10 username = socket.request.session.username;
11 if (!username) {
12 username = 'Anonymous Coward';
13 }
14 message = username + ': ' + message;
15
16 // Publish it
17 client.publish('ChatChannel', message);
18
19 // Persist it to a Redis list
20 client.rpush('chat:messages', message);
21 });

Here we use a regex to strip out any HTML tags from the message - this will prevent anyone injecting JavaScript into our chat client.

And that's all, folks! If you want to check out the source for this lesson it's in the repository on GitHub, tagged lesson-2. If you want to carry on working on this on your own, there's still plenty you can do, such as:

  • Adding support for multiple rooms
  • Using Passport.js to allow logging in using third-party services such as Twitter or Facebook
  • Adding formatting for messages, either by using something like Markdown, or a client-side rich text editor

As you can see, it's surprising how much you can accomplish using only Redis, and under certain circumstances it offers a lot of advantages over a relational database. It's always worth thinking about whether Redis can be used for your project.