ptg 14.2 The Controller 353 Unfortunately, this doesn’t play out exactly as planned. The previous test, which also calls post, is now attempting to call addMessage on chatRoom, which is undefined in that test. We can fix the issue by moving the chatRoom stub into setUp as Listing 14.18 does. Listing 14.18 Sharing the chatRoom stub function controllerSetUp() { /* */ this.controller.chatRoom = { addMessage: stub() }; } All the tests go back to a soothing green, and we can turn our attention to the duplicated logic we just introduced in the second test. In particular, both tests simulates sending a request with a body. We can simplify the tests considerably by extracting this logic into the setup. Listing 14.19 shows the updated tests. Listing 14.19 Cleaning up post tests function controllerSetUp() { /* */ this.sendRequest = function (data) { var str = encodeURI(JSON.stringify(data)); this.req.emit("data", str.substring(0, str.length / 2)); this.req.emit("data", str.substring(str.length / 2)); this.req.emit("end"); }; } testCase(exports, "chatRoomController.post", { /* */ "should parse request body as JSON": function (test) { var data = { data: { user: "cjno", message: "hi" } }; JSON.parse = stub(data); this.controller.post(); this.sendRequest(data); test.equals(JSON.parse.args[0], JSON.stringify(data)); test.done(); }, /* */ }); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 354 Server-Side JavaScript with Node.js The cleaned up tests certainly are a lot easier to follow, and with the send- Request helper method, writing new tests that make requests will be easier as well. All tests pass and we can move on. 14.2.4.3 Malicious Data Notice that we are currently accepting messages completely unfiltered. This can lead to all kinds of scary situations, for instance consider the effects of the request in Listing 14.20 Listing 14.20 Malicious request { "topic": "message", "data": { "user": "cjno", "message": "<script>window.location = 'http://hacked';</script>" } } Before deploying anapplication like the one we are currently building we should take care to not blindly accept any end user data unfiltered. 14.2.5 Responding to Requests When the controller has added the message, it should respond and close the connec- tion. In most web frameworks, output buffering and closing the connection happen automatically behind the scenes. The HTTP server support in Node, however, was consciously designed with data streaming and long polling in mind. For this reason, data is never buffered, and connections are never closed until told to do so. http.ServerResponse objects offer a few methods useful to output a re- sponse, namely writeHead, which writes the status code and response headers; write, which writes a chunk to the response body; and finally end. 14.2.5.1 Status Code As there really isn’t much feedback to give the user when a message is added, Listing 14.21 simply expects post to respond with an empty “201 Created.” Listing 14.21 Expecting status code 201 function controllerSetUp() { /* */ var res = this.res = { writeHead: stub() }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.2 The Controller 355 /* */ } testCase(exports, "chatRoomController.post", { /* */ "should write status header": function (test) { var data = { data: { user: "cjno", message: "hi" } }; this.controller.post(); this.sendRequest(data); test.ok(this.res.writeHead.called); test.equals(this.res.writeHead.args[0], 201); test.done(); } }); Listing 14.22 faces the challenge and makes the actual call to writeHead. Listing 14.22 Setting the response code post: function () { /* */ this.request.addListener("end", function () { var data = JSON.parse(decodeURI(body)).data; this.chatRoom.addMessage(data.user, data.message); this.response.writeHead(201); }.bind(this)); } 14.2.5.2 Closing the Connection Once the headers have been written, we should make sure the connection is closed. Listing 14.23 shows the test. Listing 14.23 Expecting the response to be closed function controllerSetUp() { /* */ var res = this.res = { writeHead: stub(), end: stub() }; /* */ }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 356 Server-Side JavaScript with Node.js testCase(exports, "chatRoomController.post", { /* */ "should close connection": function (test) { var data = { data: { user: "cjno", message: "hi" } }; this.controller.post(); this.sendRequest(data); test.ok(this.res.end.called); test.done(); } }); The test fails, and Listing 14.24 shows the updated post method, which passes all the tests. Listing 14.24 Closing the response post: function () { /* */ this.request.addListener("end", function () { /* */ this.response.end(); }.bind(this)); } That’s it for the post method. It is now functional enough to properly handle well-formed requests. In a real-world setting, however, I encourage more rigid input verification and error handling. Making the method more resilient is left as an exercise. 14.2.6 Taking the Application for a Spin If we make a small adjustment to the server, we can now take the application for a spin. In the original listing, the server did not set up a chatRoom for the controller. To successfully run the application, update the server to match Listing 14.25. Listing 14.25 The final server var http = require("http"); var url = require("url"); var crController = require("chapp/chat_room_controller"); var chatRoom = require("chapp/chat_room"); From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.2 The Controller 357 var room = Object.create(chatRoom); module.exports = http.createServer(function (req, res) { if (url.parse(req.url).pathname == "/comet") { var controller = crController.create(req, res); controller.chatRoom = room; controller[req.method.toLowerCase()](); } }); For this to work, we need to add a fake chatRoom module. Save the contents of Listing 14.26 to lib/chapp/chat _ room.js. Listing 14.26 A fake chat room var sys = require("sys"); var chatRoom = { addMessage: function (user, message) { sys.puts(user + ": " + message); } }; module.exports = chatRoom; Listing 14.27 shows how to use node-repl, an interactive Node shell, to encode some POST data and post it to the application using curl, the command line HTTP client. Run it in another shell, and watch the output from the shell that is running the application. Listing 14.27 Manually testing the app from the command line $ node-repl node> var msg = { user:"cjno", message:"Enjoying Node.js" }; node> var data = { topic: "message", data: msg }; node> var encoded = encodeURI(JSON.stringify(data)); node> require("fs").writeFileSync("chapp.txt", encoded); node> Ctrl-d $ curl -d `cat chapp.txt` http://localhost:8000/comet When you enter that last command, you should get an immediate response (i.e., it simply returns to your prompt) and the shell that is running the server should output “cjno: Enjoying Node.js.” In Chapter 15, TDD and DOM Manipulation: The Chat Client, we will build a proper frontend for the application. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 358 Server-Side JavaScript with Node.js 14.3 Domain Model and Storage The domain model of the chat application will consist of a single chatRoom object for the duration of our exercise. chatRoom will simply store messages in memory, but we will design it following Node’s I/O conventions. 14.3.1 Creating a Chat Room As with the controller, we will rely on Object.create to create new objects inheriting from chatRoom. However, until proved otherwise, chatRoom does not need an initializer, so we can simply create objects with Object.create directly. Should we decide to add an initializer at a later point, we must update the places that create chat room objects in the tests, which should be a good motivator to keep from duplicating the call. 14.3.2 I/O in Node Because the chatRoom interface will take the role as the storage backend, we classify it as an I/O interface. This means it should follow Node’s carefully thought out conventions for asynchronous I/O, even if it’s just an in-memory store for now. Doing so allows us to very easily refactor to use a persistence mechanism, such as a database or web service, at a later point. In Node, asynchronous interfaces accept an optional callback as their last ar- gument. The first argument passed to the callback is always either null or an error object. This removes the need for a dedicated “errback” function. Listing 14.28 shows an example using the file system module. Listing 14.28 Callback and errback convention in Node var fs = require("fs"); fs.rename("./tetx.txt", "./text.txt", function (err) { if (err) { throw err; } // Renamed successfully, carry on }); This convention is used for all low-level system interfaces, and it will be our starting point as well. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.3 Domain Model and Storage 359 14.3.3 Adding Messages As dictated by the controller using it, the chatRoom object should have an ad- dMessage method that accepts a username and a message. 14.3.3.1 Dealing with Bad Data For basic data consistency, the addMessage method should err if either the user- name or message is missing. However, as an asynchronous I/O interface, it cannot simply throw exceptions. Rather, we will expect errors to be passed as the first ar- gument to the callback registered with addMessage, as is the Node way. Listing 14.29 shows the test for missing username. Save it in test/chapp/chat _ room _ test.js. Listing 14.29 addMessage should require username var testCase = require("nodeunit").testCase; var chatRoom = require("chapp/chat_room"); testCase(exports, "chatRoom.addMessage", { "should require username": function (test) { var room = Object.create(chatRoom); room.addMessage(null, "a message", function (err) { test.isNotNull(err); test.inherits(err, TypeError); test.done(); }); } }); The test fails as expected, and so we add a check on the user parameter, as Listing 14.30 shows. Listing 14.30 Checking the username var chatRoom = { addMessage: function (user, message, callback) { if (!user) { callback(new TypeError("user is null")); } } }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 360 Server-Side JavaScript with Node.js The test passes, and we can move on to checking the message. The test in Listing 14.31 expects addMessage to require a message. Listing 14.31 addMessage should require message "should require message": function (test) { var room = Object.create(chatRoom); room.addMessage("cjno", null, function (err) { test.isNotNull(err); test.inherits(err, TypeError); test.done(); }); } The test introduces some duplication that we’ll deal with shortly. First, Listing 14.32 makes the check that passes it. Listing 14.32 Checking the message addMessage: function (user, message, callback) { /* */ if (!message) { callback(new TypeError("message is null")); } } All the tests pass. Listing 14.33 adds a setUp method to remove the duplicated creation of the chatRoom object. Listing 14.33 Adding a setUp method testCase(exports, "chatRoom.addMessage", { setUp: function () { this.room = Object.create(chatRoom); }, /* */ }); As we decided previously, the callback should be optional, so Listing 14.34 adds a test that expects the method not to fail when the callback is missing. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 14.3 Domain Model and Storage 361 Listing 14.34 Expecting addMessage not to require a callback /* */ require("function-bind"); /* */ testCase(exports, "chatRoom.addMessage", { /* */ "should not require a callback": function (test) { test.noException(function () { this.room.addMessage(); test.done(); }.bind(this)); } } Once again we load the custom bind implementation to bind the anonymous callback to test.noException. To pass the test we need to check that the callback is callable before calling it, as Listing 14.35 shows. Listing 14.35 Verifying that callback is callable before calling it addMessage: function (user, message, callback) { var err = null; if (!user) { err = new TypeError("user is null"); } if (!message) { err = new TypeError("message is null"); } if (typeof callback == "function") { callback(err); } } 14.3.3.2 Successfully Adding Messages We won’t be able to verify that messages are actually stored until we have a way to retrieve them, but we should get some indication on whether or not adding the message was successful. To do this we’ll expect the method to call the callback with a message object. The object should contain the data we passed in along with an id. The test can be seen in Listing 14.36. From the Library of WoweBook.Com Download from www.eBookTM.com ptg 362 Server-Side JavaScript with Node.js Listing 14.36 Expecting addMessage to pass the created message "should call callback with new object": function (test) { var txt = "Some message"; this.room.addMessage("cjno", txt, function (err, msg) { test.isObject(msg); test.isNumber(msg.id); test.equals(msg.message, txt); test.equals(msg.user, "cjno"); test.done(); }); } Listing 14.37 shows an attempt at passing the test. It calls the callback with an object and cheats the id by hard-coding it to 1. Listing 14.37 Passing the object to the callback addMessage: function (user, message, callback) { /* */ var data; if (!err) { data = { id: 1, user: user, message: message }; } if (typeof callback == "function") { callback(err, data); } } With this in place, the tests are back to green. Next up, the id should be unique for every message. Listing 14.38 shows the test. Listing 14.38 Expecting unique message ids "should assign unique ids to messages": function (test) { var user = "cjno"; this.room.addMessage(user, "a", function (err, msg1) { this.room.addMessage(user, "b", function (err, msg2) { test.notEquals(msg1.id, msg2.id); test.done(); }); }.bind(this)); } From the Library of WoweBook.Com Download from www.eBookTM.com . WoweBook.Com Download from www.eBookTM.com ptg 354 Server-Side JavaScript with Node.js The cleaned up tests certainly are a lot easier to follow, and with the send- Request helper method, writing new tests. stub() }; /* */ }; From the Library of WoweBook.Com Download from www.eBookTM.com ptg 356 Server-Side JavaScript with Node.js testCase(exports, "chatRoomController.post", { /* */ "should. */ this.response.end(); }.bind(this)); } That’s it for the post method. It is now functional enough to properly handle well-formed requests. In a real-world setting, however, I encourage more rigid input verification and error handling.