Bug 1071166 - Outgoing messages not escaped correctly: Add tests for new messaging pipeline. r=aleth,clokep,florian a=aleth
--- a/chat/components/public/imIConversationsService.idl
+++ b/chat/components/public/imIConversationsService.idl
@@ -70,29 +70,31 @@ interface imIConversationsService: nsISu
};
// Because of limitations in libpurple (write_conv is called without context),
// there's an implicit contract that whatever message string the conversation
// service passes to a protocol, it'll get back as the originalMessage when
// "new-text" is notified. This is required for the OTR extensions to work.
// A cancellable outgoing message. Before handing a message off to a protocol,
-// the conversation service notifies observers (typically add-ons) of an
-// outgoing message, which can be transformed or cancelled.
+// the conversation service notifies observers of `preparing-message` and
+// `sending-message` (typically add-ons) of an outgoing message, which can be
+// transformed or cancelled.
[scriptable, uuid(4391ba5c-9566-41a9-bb9b-fd0a0a490c2c)]
interface imIOutgoingMessage: nsISupports {
attribute AUTF8String message;
attribute boolean cancelled;
readonly attribute prplIConversation conversation;
};
// A cancellable message to be displayed. When the conversation service is
-// notified of a new-text (ie. an incoming or outgoing message to be displayed),
-// it in turn notifies observers (again, typically add-ons), which have the
-// opportunity to swap or cancel the message.
+// notified of a `new-text` (ie. an incoming or outgoing message to be
+// displayed), it in turn notifies observers of `received-message`
+// (again, typically add-ons), which have the opportunity to swap or cancel
+// the message.
[scriptable, uuid(3f88cc5c-6940-4eb5-a576-c65770f49ce9)]
interface imIMessage: prplIMessage {
attribute boolean cancelled;
// Holds the sender color for Chats.
// Empty string by default, it is set by the conversation binding.
attribute AUTF8String color;
// What eventually gets shown to the user.
--- a/chat/components/src/imConversations.js
+++ b/chat/components/src/imConversations.js
@@ -59,16 +59,17 @@ imMessage.prototype = {
get containsNick() this.prplMessage.containsNick,
get noLog() this.prplMessage.noLog,
get error() this.prplMessage.error,
get delayed() this.prplMessage.delayed,
get noFormat() this.prplMessage.noFormat,
get containsImages() this.prplMessage.containsImages,
get notification() this.prplMessage.notification,
get noLinkification() this.prplMessage.noLinkification,
+ get originalMessage() this.prplMessage.originalMessage,
getActions: function(aCount) this.prplMessage.getActions(aCount || {})
};
function UIConversation(aPrplConversation)
{
this._prplConv = {};
this.id = ++gLastUIConvId;
this._observers = [];
@@ -357,17 +358,17 @@ UIConversation.prototype = {
// Protocols have an opportunity here to preprocess messages before they are
// sent (eg. split long messages). If a message is split here, the split
// will be visible in the UI.
let messages = this.target.prepareForSending(om);
// Protocols can return null if they don't need to make any changes.
// (nb. passing null with retval array results in an empty array)
- if (!messages.length)
+ if (!messages || !messages.length)
messages = [om.message];
for (let msg of messages) {
// Add-ons (eg. OTR) have an opportunity to tweak or cancel the message
// at this point.
om = new OutgoingMessage(msg, this.target);
this.notifyObservers(om, "sending-message");
if (om.cancelled)
new file mode 100644
--- /dev/null
+++ b/chat/components/src/test/test_conversations.js
@@ -0,0 +1,257 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource:///modules/imServices.jsm");
+Cu.import("resource:///modules/jsProtoHelper.jsm");
+
+let imConversations = {};
+Services.scriptloader.loadSubScript(
+ "resource:///components/imConversations.js", imConversations
+);
+
+// Fake prplConversation
+let _id = 0;
+function Conversation(aName) {
+ this._name = aName;
+ this._observers = [];
+ this._date = Date.now() * 1000;
+ this.id = ++_id;
+}
+Conversation.prototype = {
+ __proto__: GenericConvIMPrototype,
+ _account: {
+ imAccount: {
+ protocol: {name: "Fake Protocol"},
+ alias: "",
+ name: "Fake Account"
+ },
+ ERROR: function(e) {throw e;}
+ },
+ addObserver: function(aObserver) {
+ if (!(aObserver instanceof Ci.nsIObserver))
+ aObserver = {observe: aObserver};
+ GenericConvIMPrototype.addObserver.call(this, aObserver);
+ }
+};
+
+// Ensure that when iMsg.message is set to a message (including the empty
+// string), it returns that message. If not, it should return the original
+// message. This prevents regressions due to JS coercions.
+let test_null_message = function() {
+ let originalMessage = "Hi!";
+ let pMsg = new Message("buddy", originalMessage, {
+ outgoing: true, _alias: "buddy", time: Date.now()
+ });
+ let iMsg = new imConversations.imMessage(pMsg);
+ equal(iMsg.message, originalMessage, "Expected the original message.");
+ // Setting the message should prevent a fallback to the original.
+ iMsg.message = "";
+ equal(iMsg.message, "", "Expected an empty string; not the original message.");
+ equal(iMsg.originalMessage, originalMessage, "Expected the original message.");
+};
+
+// ROT13, used as an example transformation.
+function rot13(aString) {
+ return aString.replace(/[a-zA-Z]/g, function(c) {
+ return String.fromCharCode(c.charCodeAt(0) + (c.toLowerCase() < "n" ? 1 : -1) * 13);
+ });
+}
+
+// A test that exercises the message transformation pipeline.
+//
+// From the sending users perspective, this looks like:
+// -> UIConv sendMsg
+// -> UIConv notifyObservers `preparing-message`
+// -> protocol prepareForSending
+// -> UIConv notifyObservers `sending-message`
+// -> protocol sendMsg
+// -> protocol writeMessage
+// -> protocol notifyObservers `new-text`
+// -> UIConv notifyObservers `received-message`
+// -> protocol prepareForDisplaying
+// -> UIConv notifyObservers `new-text`
+//
+// From the receiving users perspective, they get:
+// -> protocol writeMessage
+// -> protocol notifyObservers `new-text`
+// -> UIConv notifyObservers `received-message`
+// -> protocol prepareForDisplaying
+// -> UIConv notifyObservers `new-text`
+//
+// The test walks the sending path, which covers both.
+let test_message_transformation = function() {
+ let conv = new Conversation();
+ conv.sendMsg = function(aMsg) {
+ this.writeMessage("user", aMsg, {outgoing: true});
+ };
+
+ let uiConv = new imConversations.UIConversation(conv);
+ let message = "Hello!";
+ let receivedMsg = false, newTxt = false;
+
+ uiConv.addObserver({
+ observe: function(aObject, aTopic, aMsg) {
+ switch(aTopic) {
+ case "sending-message":
+ ok(!newTxt, "sending-message should fire before new-text.");
+ ok(!receivedMsg, "sending-message should fire before received-message.");
+ ok(aObject.QueryInterface(Ci.imIOutgoingMessage), "Wrong message type.");
+ aObject.message = rot13(aObject.message);
+ break;
+ case "received-message":
+ ok(!newTxt, "received-message should fire before new-text.");
+ ok(!receivedMsg, "Sanity check that receive-message hasn't fired yet.");
+ ok(aObject.outgoing, "Expected an outgoing message.");
+ ok(aObject.QueryInterface(Ci.imIMessage), "Wrong message type.");
+ equal(aObject.displayMessage, rot13(message), "Expected to have been rotated while sending-message.");
+ aObject.displayMessage = rot13(aObject.displayMessage);
+ receivedMsg = true;
+ break;
+ case "new-text":
+ ok(!newTxt, "Sanity check that new-text hasn't fired yet.");
+ ok(receivedMsg, "Expected received-message to have fired.");
+ ok(aObject.outgoing, "Expected an outgoing message.");
+ ok(aObject.QueryInterface(Ci.imIMessage), "Wrong message type.");
+ equal(aObject.displayMessage, message, "Expected to have been rotated back to msg in received-message.");
+ newTxt = true;
+ break;
+ }
+ }
+ });
+
+ uiConv.sendMsg(message);
+ ok(newTxt, "Expected new-text to have fired.");
+};
+
+// A test that cancels a message before it can be sent.
+let test_cancel_send_message = function() {
+ let conv = new Conversation();
+ conv.sendMsg = function(aMsg) {
+ ok(false, "The message should have been halted in the conversation service.");
+ };
+
+ let sending = false;
+ let uiConv = new imConversations.UIConversation(conv);
+ uiConv.addObserver({
+ observe: function(aObject, aTopic, aMsg) {
+ switch(aTopic) {
+ case "sending-message":
+ ok(aObject.QueryInterface(Ci.imIOutgoingMessage), "Wrong message type.");
+ aObject.cancelled = true;
+ sending = true;
+ break;
+ case "received-message":
+ case "new-text":
+ ok(false, "No other notification should be fired for a cancelled message.");
+ break;
+ }
+ }
+ });
+ uiConv.sendMsg("Hi!");
+ ok(sending, "The sending-message notification was never fired.");
+};
+
+// A test that cancels a message before it gets displayed.
+let test_cancel_display_message = function() {
+ let conv = new Conversation();
+ conv.sendMsg = function(aMsg) {
+ this.writeMessage("user", aMsg, {outgoing: true});
+ };
+
+ let received = false;
+ let uiConv = new imConversations.UIConversation(conv);
+ uiConv.addObserver({
+ observe: function(aObject, aTopic, aMsg) {
+ switch(aTopic) {
+ case "received-message":
+ ok(aObject.QueryInterface(Ci.imIMessage), "Wrong message type.");
+ aObject.cancelled = true;
+ received = true;
+ break;
+ case "new-text":
+ ok(false, "Should not fire for a cancelled message.");
+ break;
+ }
+ }
+ });
+
+ uiConv.sendMsg("Hi!");
+ ok(received, "The received-message notification was never fired.")
+};
+
+// A test that ensures protocols get a chance to prepare a message before
+// sending and displaying.
+let test_prpl_message_prep = function() {
+ let conv = new Conversation();
+ conv.sendMsg = function(aMsg) {
+ this.writeMessage("user", aMsg, {outgoing: true});
+ };
+
+ let msg = "Hi!";
+ let prefix = "test> ";
+
+ let prepared = false;
+ conv.prepareForSending = function(aMsg) {
+ ok(aMsg.QueryInterface(Ci.imIOutgoingMessage), "Wrong message type.");
+ equal(aMsg.message, msg, "Expected the original message.");
+ aMsg.message = prefix + aMsg.message;
+ prepared = true;
+ };
+
+ conv.prepareForDisplaying = function(aMsg) {
+ ok(aMsg.QueryInterface(Ci.imIMessage), "Wrong message type.");
+ equal(aMsg.displayMessage, prefix + msg, "Expected the prefixed message.");
+ aMsg.displayMessage = aMsg.displayMessage.slice(prefix.length);
+ };
+
+ let receivedMsg = false;
+ let uiConv = new imConversations.UIConversation(conv);
+ uiConv.addObserver({
+ observe: function(aObject, aTopic, aMsg) {
+ if (aTopic === "new-text") {
+ ok(prepared, "The message was not prepared before sending.");
+ equal(aObject.displayMessage, msg, "Expected the original message.");
+ receivedMsg = true;
+ }
+ }
+ });
+
+ uiConv.sendMsg(msg);
+ ok(receivedMsg, "The received-message notification was never fired.");
+};
+
+// A test that ensures protocols can split messages before they are sent.
+let test_split_message_before_sending = function() {
+ let msgCount = 0;
+ let prepared = false;
+
+ let msg = "This is a looo\nooong message.\nThis one is short.";
+ let msgs = msg.split("\n");
+
+ let conv = new Conversation();
+ conv.sendMsg = function(aMsg) {
+ equal(aMsg, msgs[msgCount++], "Sending an unexpected message.");
+ };
+ conv.prepareForSending = function(aMsg) {
+ ok(aMsg.QueryInterface(Ci.imIOutgoingMessage), "Wrong message type.");
+ prepared = true;
+ return aMsg.message.split("\n");
+ };
+
+ let uiConv = new imConversations.UIConversation(conv);
+ uiConv.sendMsg(msg);
+
+ ok(prepared, "Message wasn't prepared for sending.");
+ equal(msgCount, 3, "Not enough messages were sent.");
+};
+
+function run_test() {
+ test_null_message();
+ test_message_transformation();
+ test_cancel_send_message();
+ test_cancel_display_message();
+ test_prpl_message_prep();
+ test_split_message_before_sending();
+ run_next_test();
+}
--- a/chat/components/src/test/xpcshell.ini
+++ b/chat/components/src/test/xpcshell.ini
@@ -3,9 +3,10 @@
; file, You can obtain one at http://mozilla.org/MPL/2.0/.
[DEFAULT]
head =
tail =
[test_accounts.js]
[test_commands.js]
+[test_conversations.js]
[test_logger.js]