Bug 1071166 - Outgoing messages not escaped correctly: Add tests for new messaging pipeline. r=aleth,clokep,florian a=aleth
authorArlo Breault <arlolra@gmail.com>
Sun, 26 Oct 2014 17:43:52 -0700 (2014-10-27)
changeset 17304 d6f2f1a42c74a03dd57f244ccd171d428534893a
parent 17303 90bc651566be85aeff9ae8baed127314e1fd3c39
child 17305 43cc5528d919bf77b066840ecb73dfc8702e6ea5
push id10679
push useraleth@instantbird.org
push dateMon, 05 Jan 2015 19:06:44 +0000 (2015-01-05)
treeherdercomm-central@d6f2f1a42c74 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaleth, clokep, florian, aleth
bugs1071166
Bug 1071166 - Outgoing messages not escaped correctly: Add tests for new messaging pipeline. r=aleth,clokep,florian a=aleth
chat/components/public/imIConversationsService.idl
chat/components/src/imConversations.js
chat/components/src/test/test_conversations.js
chat/components/src/test/xpcshell.ini
--- 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]