Listing 14.54 Expecting chatRoom to be event emitter testCaseexports, "chatRoom", { "should be event emitter": function test { test.isFunctionchatRoom.addListener; test.isFunctionchatRoo
Trang 1Listing 14.54 Expecting chatRoom to be event emitter
testCase(exports, "chatRoom", {
"should be event emitter": function (test) { test.isFunction(chatRoom.addListener);
test.isFunction(chatRoom.emit);
test.done();
} });
We can pass this test by popping EventEmitter.prototype in as chat-Room’s prototype, as seen in Listing 14.55
Listing 14.55 chatRoom inheriting from EventEmitter.prototype
/* */
var EventEmitter = require("events").EventEmitter;
/* */
var chatRoom = Object.create(EventEmitter.prototype);
chatRoom.addMessage = function (user, message) {/* */};
chatRoom.getMessagesSince = function (id) {/* */};
Note that because V8 fully supports ECMAScript 5’s Object.create, we could have used property descriptors to add the methods as well, as seen in Listing 14.56
Listing 14.56 chatRoom defined with property descriptors
var chatRoom = Object.create(EventEmitter.prototype, { addMessage: {
value: function (user, message) { /* */
} }, getMessagesSince: { value: function (id) { /* */
} } });
Trang 2At this point the property descriptors don’t provide anything we have a doc-umented need for (i.e., the ability to override default property attribute values),
so we’ll avoid the added indentation and stick with the simple assignments in
Listing 14.55
Next up, we make sure that addMessage emits an event Listing 14.57 shows the test
Listing 14.57 Expecting addMessage to emit a “message” event
testCase(exports, "chatRoom.addMessage", {
/* */
"should emit 'message' event": function (test) { var message;
this.room.addListener("message", function (m) { message = m;
});
this.room.addMessage("cjno", "msg").then(function (m) { test.same(m, message);
test.done();
});
} });
To pass this test we need to place a call to emit right before we resolve the promise, as seen in Listing 14.58
Listing 14.58 Emitting a message event
chatRoom.addMessage= function (user, message, callback) {
var promise = new Promise() process.nextTick(function () { /* */
if (!err) { /* */
this.emit("message", data);
promise.resolve(data);
} else { promise.reject(err, true);
}
Trang 3}.bind(this));
return promise;
};
With the event in place, we can build the waitForMessagesSince method
14.5.2 Waiting for Messages
The waitForMessagesSince method will do one of two things; if messages are available since the provided id, the returned promise will resolve immediately If no messages are currently available, the method will add a listener for the “message”
event, and the returned promise will resolve once a new message is added
The test in Listing 14.59 expects that the promise is immediately resolved if messages are available
Listing 14.59 Expecting available messages to resolve immediately
/* */
var Promise = require("node-promise/promise").Promise;
var stub = require("stub");
/* */
testCase(exports, "chatRoom.waitForMessagesSince", { setUp: chatRoomSetup,
"should yield existing messages": function (test) { var promise = new Promise();
promise.resolve([{ id: 43 }]);
this.room.getMessagesSince = stub(promise);
this.room.waitForMessagesSince(42).then(function (m) { test.same([{ id: 43 }], m);
test.done();
});
} });
This test stubs the getMessagesSince method to verify that its results are used if there are any To pass this test we can simply return the promise returned from getMessagesSince, as seen in Listing 14.60
Trang 4Listing 14.60 Proxying getMessagesSince
chatRoom.waitForMessagesSince = function (id) {
return this.getMessagesSince(id);
};
Now to the interesting part If the attempt to fetch existing methods does not succeed, the method should add a listener for the “message” event and go to sleep
Listing 14.61 tests this by stubbing addListener
Listing 14.61 Expecting the wait method to add a listener
"should add listener when no messages": function (test) {
this.room.addListener = stub();
var promise = new Promise();
promise.resolve([]);
this.room.getMessagesSince = stub(promise);
this.room.waitForMessagesSince(0);
process.nextTick(function () { test.equals(this.room.addListener.args[0], "message");
test.isFunction(this.room.addListener.args[1]);
test.done();
}.bind(this));
}
Again we stub the getMessagesSince method to control its output We then resolve the promise it’s stubbed to return, passing an empty array This
should cause the waitForMessagesSince method to register a listener for
the “message” event Seeing as waitForMessagesSince does not add a
lis-tener, the test fails To pass it, we need to change the implementation as seen in
Listing 14.62
Listing 14.62 Adding a listener if no messages are available
chatRoom.waitForMessagesSince = function (id) {
var promise = new Promise();
this.getMessagesSince(id).then(function (messages) {
if (messages.length > 0) { promise.resolve(messages);
} else { this.addListener("message", function () {});
}
Trang 5}.bind(this));
return promise;
};
The listener we just added is empty, as we don’t yet have a test that tells us what
it needs to do That seems like a suitable topic for the next test, which will assert that adding a message causes waitForMessagesSince to resolve with the new message For symmetry with getMessagesSince, we expect the single message
to arrive as an array Listing 14.63 shows the test
Listing 14.63 Adding a message should resolve waiting requests
"new message should resolve waiting": function (test) { var user = "cjno";
var msg = "Are you waiting for this?";
this.room.waitForMessagesSince(0).then(function (msgs) { test.isArray(msgs);
test.equals(msgs.length, 1);
test.equals(msgs[0].user, user);
test.equals(msgs[0].message, msg);
test.done();
});
process.nextTick(function () { this.room.addMessage(user, msg);
}.bind(this));
}
Unsurprisingly, the test does not pass, prompting us to fill in the “message”
listener we just added Listing 14.64 shows the working listener
Listing 14.64 Implementing the message listener
/* */
this.addListener("message", function (message) { promise.resolve([message]);
});
/* */
And that’s all it takes, the tests all pass, and our very rudimentary data layer is complete enough to serve its purpose in the application Still, there is one very im-portant task to complete, and one that I will leave as an exercise Once the promise
Trang 6returned from waitForMessagesSince is resolved, the listener added to the
“message” event needs to be cleared Otherwise, the original call to
waitForMes-sagesSincewill have its callback called every time a message is added, even after
the current request has ended
To do this you will need a reference to the function added as a handler, and use this.removeListener To test it, it will be helpful to know that
room.listeners()returns the array of listeners, for your inspection pleasure
14.6 Returning to the Controller
With a functional data layer we can get back to finishing the controller We’re going
to give post the final polish and implement get
14.6.1 Finishing the post Method
The post method currently responds with the 201 status code, regardless of
whether the message was added or not, which is in violation with the
seman-tics of a 201 response; the HTTP spec states that “The origin server MUST
cre-ate the resource before returning the 201 status code.” Having implemented the
addMessagemethod we know that this is not necessarily the case in our current
implementation Let’s get right on fixing that
The test that expects post to call writeHead needs updating We now expect the headers to be written once the addMessage method resolves Listing 14.65
shows the updated test
Listing 14.65 Expecting post to respond immediately when addMessage
resolves
/* */
var Promise = require("node-promise/promise").Promise;
/* */
function controllerSetUp() {
/* */
var promise = this.addMessagePromise = new Promise();
this.controller.chatRoom = { addMessage: stub(promise) };
/* */
}
/* */
testCase(exports, "chatRoomController.post", {
Trang 7/* */
"should write status header when addMessage resolves":
function (test) { var data = { data: { user: "cjno", message: "hi" } };
this.controller.post();
this.sendRequest(data);
this.addMessagePromise.resolve({});
process.nextTick(function () { test.ok(this.res.writeHead.called);
test.equals(this.res.writeHead.args[0], 201);
test.done();
}.bind(this));
}, /* */
});
Delaying the verification doesn’t affect the test very much, so the fact that
it still passes only tells us none of the new setup code is broken We can apply the same update to the following test, which expects the connection to be closed
Listing 14.66 shows the updated test
Listing 14.66 Expecting post not to close connection immediately
"should close connection when addMessage resolves":
function (test) { var data = { data: { user: "cjno", message: "hi" } };
this.controller.post();
this.sendRequest(data);
this.addMessagePromise.resolve({});
process.nextTick(function () { test.ok(this.res.end.called);
test.done();
}.bind(this));
}
Listing 14.67 shows a new test, which contradicts the two tests the way they
were previously written This test specifically expects the action not to respond
before addMessage has resolved
Trang 8Listing 14.67 Expecting post not to respond immediately
"should not respond immediately": function (test) {
this.controller.post();
this.sendRequest({ data: {} });
test.ok(!this.res.end.called);
test.done();
}
This test does not run as smoothly as the previous two Passing it is a matter
of deferring the closing calls until the promise returned by addMessage resolves
Listing 14.68 has the lowdown
Listing 14.68 post responds when addMessage resolves
post: function () {
/* */
this.request.addListener("end", function () { var data = JSON.parse(decodeURI(body)).data;
this.chatRoom.addMessage(
data.user, data.message ).then(function () { this.response.writeHead(201);
this.response.end();
}.bind(this));
}.bind(this));
}
That’s about it for the post method Note that the method does not handle errors in any way; in fact it will respond with a 201 status even if the message was
not added successfully I’ll leave fixing it as an exercise
14.6.2 Streaming Messages with GET
GET requests should either be immediately responded to with messages, or held
open until messages are available Luckily, we did most of the heavy lifting while
implementing chatRoom.waitForMessagesSince, so the get method of the
controller will simply glue together the request and the data
Trang 914.6.2.1 Filtering Messages with Access Tokens
Remember how the cometClient from Chapter 13, Streaming Data with Ajax
and Comet, informs the server of what data to retrieve? We set it up to use the
X-Access-Tokenheader, which can contain any value and is controlled by the server Because we built waitForMessagesSince to use ids, it should not come
as a surprise that we are going to track progress using them
When a client connects for the first time, it’s going to send an empty X-Access-Token, so handling that case seems like a good start Listing 14.69 shows the test for the initial attempt We expect the controller to simply return all available messages on first attempt, meaning that empty access token should imply waiting for messages since 0
Listing 14.69 Expecting the client to grab all messages
testCase(exports, "chatRoomController.get", { setUp: controllerSetUp,
tearDown: controllerTearDown,
"should wait for any message": function (test) { this.req.headers = { "x-access-token": "" };
var chatRoom = this.controller.chatRoom;
chatRoom.waitForMessagesSince = stub();
this.controller.get();
test.ok(chatRoom.waitForMessagesSince.called);
test.equals(chatRoom.waitForMessagesSince.args[0], 0);
test.done();
} });
Notice that Node downcases the headers Failing to recognize this may take away some precious minutes from your life Or so I’ve heard To pass this test we can cheat by passing the expected id directly to the method, as Listing 14.70 does
Listing 14.70 Cheating to pass tests
var chatRoomController = { /* */
get: function () { this.chatRoom.waitForMessagesSince(0);
} };
Trang 10The test passes Onward to the subsequent requests, which should be coming
in with an access token Listing 14.71 stubs the access token with an actual value,
and expects this to be passed to waitForMessagesSince
Listing 14.71 Expecting get to pass the access token
"should wait for messages since X-Access-Token":
function (test) {
this.req.headers = { "x-access-token": "2" };
var chatRoom = this.controller.chatRoom;
chatRoom.waitForMessagesSince = stub();
this.controller.get();
test.ok(chatRoom.waitForMessagesSince.called);
test.equals(chatRoom.waitForMessagesSince.args[0], 2);
test.done();
}
This test looks a lot like the previous one, only it expects the passed id to be the same as provided with the X-Access-Token header These tests could need
some cleaning up, and I encourage you to give them a spin Passing the test is simple,
as Listing 14.72 shows
Listing 14.72 Passing the access token header
get: function () {
var id = this.request.headers["x-access-token"] || 0;
this.chatRoom.waitForMessagesSince(id);
}
14.6.2.2 The respond Method
Along with the response body, which should be a JSON response of some kind, the
getmethod should also send status code and possibly some response headers, and
finally close the connection This sounds awfully similar to what post is currently
doing We’ll extract the response into a new method in order to reuse it with the get
request Listing 14.73 shows two test cases for it, copied from the post test case
Listing 14.73 Initial tests for respond
testCase(exports, "chatRoomController.respond", {
setUp: controllerSetUp,
"should write status code": function (test) { this.controller.respond(201);
Trang 11test.ok(this.res.writeHead.called);
test.equals(this.res.writeHead.args[0], 201);
test.done();
},
"should close connection": function (test) { this.controller.respond(201);
test.ok(this.res.end.called);
test.done();
} });
We can pass these tests by copying the two lines we last added to post into the new respond method, as Listing 14.74 shows
Listing 14.74 A dedicated respond method
var chatRoomController = { /* */
respond: function (status) { this.response.writeHead(status);
this.response.end();
} };
Now we can simplify the post method by calling this method instead Doing
so also allows us to merge the original tests for status code and connection closing,
by stubbing respond and asserting that it was called
14.6.2.3 Formatting Messages
Next up for the get method is properly formatting messages Again we’ll need to lean on the cometClient, which defines the data format The method should respond with a JSON object whose properties name the topic and values are arrays
of objects Additionally, the JSON object should include a token property The JSON string should be written to the response body
We can formulate this as a test by stubbing respond as we did before, this time expecting an object passed as the second argument Thus, we will need to embellish respond later, having it write its second argument to the response body
as a JSON string Listing 14.75 shows the test