summaryrefslogtreecommitdiffstats
path: root/comm/chat/components/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/components/src/test')
-rw-r--r--comm/chat/components/src/test/test_accounts.js48
-rw-r--r--comm/chat/components/src/test/test_commands.js271
-rw-r--r--comm/chat/components/src/test/test_conversations.js239
-rw-r--r--comm/chat/components/src/test/test_init.js28
-rw-r--r--comm/chat/components/src/test/test_logger.js860
-rw-r--r--comm/chat/components/src/test/xpcshell.ini9
6 files changed, 1455 insertions, 0 deletions
diff --git a/comm/chat/components/src/test/test_accounts.js b/comm/chat/components/src/test/test_accounts.js
new file mode 100644
index 0000000000..267095455f
--- /dev/null
+++ b/comm/chat/components/src/test/test_accounts.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { IMServices } = ChromeUtils.importESModule(
+ "resource:///modules/IMServices.sys.mjs"
+);
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+function run_test() {
+ do_get_profile();
+
+ // Test the handling of accounts for unknown protocols.
+ const kAccountName = "Unknown";
+ const kPrplId = "prpl-unknown";
+
+ let prefs = Services.prefs;
+ prefs.setCharPref("messenger.account.account1.name", kAccountName);
+ prefs.setCharPref("messenger.account.account1.prpl", kPrplId);
+ prefs.setCharPref("mail.accountmanager.accounts", "account1");
+ prefs.setCharPref("mail.account.account1.server", "server1");
+ prefs.setCharPref("mail.server.server1.imAccount", "account1");
+ prefs.setCharPref("mail.server.server1.type", "im");
+ prefs.setCharPref("mail.server.server1.userName", kAccountName);
+ prefs.setCharPref("mail.server.server1.hostname", kPrplId);
+ try {
+ // Having an implementation of nsIXULAppInfo is required for
+ // IMServices.core.init to work.
+ updateAppInfo();
+ IMServices.core.init();
+
+ let account = IMServices.accounts.getAccountByNumericId(1);
+ Assert.ok(account instanceof Ci.imIAccount);
+ Assert.equal(account.name, kAccountName);
+ Assert.equal(account.normalizedName, kAccountName);
+ Assert.equal(account.protocol.id, kPrplId);
+ Assert.equal(
+ account.connectionErrorReason,
+ Ci.imIAccount.ERROR_UNKNOWN_PRPL
+ );
+ } finally {
+ IMServices.core.quit();
+
+ prefs.deleteBranch("messenger");
+ }
+}
diff --git a/comm/chat/components/src/test/test_commands.js b/comm/chat/components/src/test/test_commands.js
new file mode 100644
index 0000000000..de0fd0e665
--- /dev/null
+++ b/comm/chat/components/src/test/test_commands.js
@@ -0,0 +1,271 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { IMServices } = ChromeUtils.importESModule(
+ "resource:///modules/IMServices.sys.mjs"
+);
+// We don't load the command service via Services as we want to access
+// _findCommands in order to avoid having to intercept command execution.
+var { CommandsService } = ChromeUtils.importESModule(
+ "resource:///modules/imCommands.sys.mjs"
+);
+
+var kPrplId = "green";
+var kPrplId2 = "red";
+
+var fakeAccount = {
+ connected: true,
+ protocol: { id: kPrplId },
+};
+var fakeDisconnectedAccount = {
+ connected: false,
+ protocol: { id: kPrplId },
+};
+var fakeAccount2 = {
+ connected: true,
+ protocol: { id: kPrplId2 },
+};
+
+var fakeConversation = {
+ account: fakeAccount,
+ isChat: true,
+};
+
+function fakeCommand(aName, aUsageContext) {
+ this.name = aName;
+ if (aUsageContext) {
+ this.usageContext = aUsageContext;
+ }
+}
+fakeCommand.prototype = {
+ get helpString() {
+ return "";
+ },
+ usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
+ priority: Ci.imICommand.CMD_PRIORITY_PRPL,
+ run: (aMsg, aConv) => true,
+};
+
+function run_test() {
+ let cmdserv = new CommandsService();
+ cmdserv.initCommands();
+
+ // Some commands providing multiple possible completions.
+ cmdserv.registerCommand(new fakeCommand("banana"), kPrplId2);
+ cmdserv.registerCommand(new fakeCommand("baloney"), kPrplId2);
+
+ // MUC-only command.
+ cmdserv.registerCommand(
+ new fakeCommand("balderdash", Ci.imICommand.CMD_CONTEXT_CHAT),
+ kPrplId
+ );
+
+ // Name clashes with global command.
+ cmdserv.registerCommand(new fakeCommand("offline"), kPrplId);
+
+ // Name starts with another command name.
+ cmdserv.registerCommand(new fakeCommand("helpme"), kPrplId);
+
+ // Command name contains numbers.
+ cmdserv.registerCommand(new fakeCommand("r9kbeta"), kPrplId);
+
+ // Array of (possibly partial) command names as entered by the user.
+ let testCmds = [
+ "x",
+ "b",
+ "ba",
+ "bal",
+ "back",
+ "hel",
+ "help",
+ "off",
+ "offline",
+ ];
+
+ // We test an array of different possible conversations.
+ // cmdlist lists all the available commands for the given conversation.
+ // results is an array which for each testCmd provides an array containing
+ // data with which the return value of _findCommands can be checked. In
+ // particular, the name of the command and whether the first (i.e. preferred)
+ // entry in the returned array of commands is a prpl command. (If the latter
+ // boolean is not given, false is assumed, if the name is not given, that
+ // corresponds to no commands being returned.)
+ let testData = [
+ {
+ desc: "No conversation argument.",
+ cmdlist: "away, back, busy, dnd, help, offline, raw, say",
+ results: [
+ [],
+ [],
+ ["back"],
+ [],
+ ["back"],
+ ["help"],
+ ["help"],
+ ["offline"],
+ ["offline"],
+ ],
+ },
+ {
+ desc: "Disconnected conversation with fakeAccount.",
+ conv: {
+ account: fakeDisconnectedAccount,
+ },
+ cmdlist:
+ "away, back, busy, dnd, help, helpme, offline, offline, r9kbeta, raw, say",
+ results: [
+ [],
+ [],
+ ["back"],
+ [],
+ ["back"],
+ ["help"],
+ ["help"],
+ ["offline"],
+ ["offline"],
+ ],
+ },
+ {
+ desc: "Conversation with fakeAccount.",
+ conv: {
+ account: fakeAccount,
+ },
+ cmdlist:
+ "away, back, busy, dnd, help, helpme, offline, offline, r9kbeta, raw, say",
+ results: [
+ [],
+ [],
+ ["back"],
+ [],
+ ["back"],
+ [],
+ ["help"],
+ ["offline"],
+ ["offline"],
+ ],
+ },
+ {
+ desc: "MUC with fakeAccount.",
+ conv: {
+ account: fakeAccount,
+ isChat: true,
+ },
+ cmdlist:
+ "away, back, balderdash, busy, dnd, help, helpme, offline, offline, r9kbeta, raw, say",
+ results: [
+ [],
+ [],
+ [],
+ ["balderdash", true],
+ ["back"],
+ [],
+ ["help"],
+ ["offline"],
+ ["offline"],
+ ],
+ },
+ {
+ desc: "Conversation with fakeAccount2.",
+ conv: {
+ account: fakeAccount2,
+ },
+ cmdlist:
+ "away, back, baloney, banana, busy, dnd, help, offline, raw, say",
+ results: [
+ [],
+ [],
+ [],
+ ["baloney", true],
+ ["back"],
+ ["help"],
+ ["help"],
+ ["offline"],
+ ["offline"],
+ ],
+ },
+ {
+ desc: "MUC with fakeAccount2.",
+ conv: {
+ account: fakeAccount2,
+ isChat: true,
+ },
+ cmdlist:
+ "away, back, baloney, banana, busy, dnd, help, offline, raw, say",
+ results: [
+ [],
+ [],
+ [],
+ ["baloney", true],
+ ["back"],
+ ["help"],
+ ["help"],
+ ["offline"],
+ ["offline"],
+ ],
+ },
+ ];
+
+ for (let test of testData) {
+ info("The following tests are with: " + test.desc);
+
+ // Check which commands are available in which context.
+ let cmdlist = cmdserv
+ .listCommandsForConversation(test.conv)
+ .map(aCmd => aCmd.name)
+ .sort()
+ .join(", ");
+ Assert.equal(cmdlist, test.cmdlist);
+
+ for (let testCmd of testCmds) {
+ info("Testing command found for '" + testCmd + "'");
+ let expectedResult = test.results.shift();
+ let cmdArray = cmdserv._findCommands(test.conv, testCmd);
+ // Check whether commands are only returned when appropriate.
+ Assert.equal(cmdArray.length > 0, expectedResult.length > 0);
+ if (cmdArray.length) {
+ // Check if the right command was returned.
+ Assert.equal(cmdArray[0].name, expectedResult[0]);
+ Assert.equal(
+ cmdArray[0].priority == Ci.imICommand.CMD_PRIORITY_PRPL,
+ !!expectedResult[1]
+ );
+ }
+ }
+ }
+
+ // Array of messages to test command execution of.
+ let testMessages = [
+ {
+ message: "/r9kbeta",
+ result: true,
+ },
+ {
+ message: "/helpme 2 arguments",
+ result: true,
+ },
+ {
+ message: "nocommand",
+ result: false,
+ },
+ {
+ message: "/-a",
+ result: false,
+ },
+ {
+ message: "/notregistered",
+ result: false,
+ },
+ ];
+
+ // Test command execution.
+ for (let executionTest of testMessages) {
+ info("Testing command execution for '" + executionTest.message + "'");
+ Assert.equal(
+ cmdserv.executeCommand(executionTest.message, fakeConversation),
+ executionTest.result
+ );
+ }
+
+ cmdserv.unInitCommands();
+}
diff --git a/comm/chat/components/src/test/test_conversations.js b/comm/chat/components/src/test/test_conversations.js
new file mode 100644
index 0000000000..c1ede89734
--- /dev/null
+++ b/comm/chat/components/src/test/test_conversations.js
@@ -0,0 +1,239 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var { IMServices } = ChromeUtils.importESModule(
+ "resource:///modules/IMServices.sys.mjs"
+);
+var { GenericConvIMPrototype, Message } = ChromeUtils.importESModule(
+ "resource:///modules/jsProtoHelper.sys.mjs"
+);
+var { imMessage, UIConversation } = ChromeUtils.importESModule(
+ "resource:///modules/imConversations.sys.mjs"
+);
+
+// Fake prplConversation
+var _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(e) {
+ throw e;
+ },
+ DEBUG() {},
+ },
+ addObserver(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.
+var test_null_message = function () {
+ let originalMessage = "Hi!";
+ let pMsg = new Message(
+ "buddy",
+ originalMessage,
+ {
+ outgoing: true,
+ _alias: "buddy",
+ time: Date.now(),
+ },
+ null
+ );
+ let iMsg = new 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:
+// -> protocol sendMsg
+// -> protocol notifyObservers `preparing-message`
+// -> protocol prepareForSending
+// -> protocol notifyObservers `sending-message`
+// -> protocol dispatchMessage (jsProtoHelper specific)
+// -> 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.
+add_task(function test_message_transformation() {
+ let conv = new Conversation();
+ conv.dispatchMessage = function (aMsg) {
+ this.writeMessage("user", aMsg, { outgoing: true });
+ };
+
+ let message = "Hello!";
+ let receivedMsg = false,
+ newTxt = false;
+
+ let uiConv = new UIConversation(conv);
+ uiConv.addObserver({
+ observe(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 gets displayed.
+add_task(function test_cancel_display_message() {
+ let conv = new Conversation();
+ conv.dispatchMessage = function (aMsg) {
+ this.writeMessage("user", aMsg, { outgoing: true });
+ };
+
+ let received = false;
+ let uiConv = new UIConversation(conv);
+ uiConv.addObserver({
+ observe(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.");
+});
+
+var test_update_message = function () {
+ let conv = new Conversation();
+
+ let uiConv = new UIConversation(conv);
+ let message = "Hello!";
+ let receivedMsg = false;
+ let updateText = false;
+
+ uiConv.addObserver({
+ observe(aObject, aTopic, aMsg) {
+ switch (aTopic) {
+ case "received-message":
+ ok(!updateText, "received-message should fire before update-text.");
+ ok(
+ !receivedMsg,
+ "Sanity check that receive-message hasn't fired yet."
+ );
+ ok(aObject.incoming, "Expected an incoming message.");
+ ok(aObject.QueryInterface(Ci.imIMessage), "Wrong message type.");
+ equal(aObject.displayMessage, message, "Wrong message contents");
+ aObject.displayMessage = rot13(aObject.displayMessage);
+ receivedMsg = true;
+ break;
+ case "update-text":
+ ok(!updateText, "Sanity check that update-text hasn't fired yet.");
+ ok(receivedMsg, "Expected received-message to have fired.");
+ ok(aObject.incoming, "Expected an incoming message.");
+ ok(aObject.QueryInterface(Ci.imIMessage), "Wrong message type.");
+ equal(
+ aObject.displayMessage,
+ rot13(message),
+ "Expected to have been rotated in received-message."
+ );
+ updateText = true;
+ break;
+ }
+ },
+ });
+
+ conv.updateMessage("user", message, { incoming: true, remoteId: "foo" });
+ ok(updateText, "Expected update-text to have fired.");
+};
+
+add_task(test_null_message);
+add_task(test_update_message);
diff --git a/comm/chat/components/src/test/test_init.js b/comm/chat/components/src/test/test_init.js
new file mode 100644
index 0000000000..48f064027f
--- /dev/null
+++ b/comm/chat/components/src/test/test_init.js
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { IMServices } = ChromeUtils.importESModule(
+ "resource:///modules/IMServices.sys.mjs"
+);
+
+// Modules that should only be loaded once a chat account exists.
+var ACCOUNT_MODULES = new Set([
+ "resource:///modules/matrixAccount.sys.mjs",
+ "resource:///modules/matrix-sdk.sys.mjs",
+ "resource:///modules/ircAccount.sys.mjs",
+ "resource:///modules/ircHandlers.sys.mjs",
+ "resource:///modules/xmpp-base.sys.mjs",
+ "resource:///modules/xmpp-session.sys.mjs",
+]);
+
+add_task(function test_coreInitLoadedModules() {
+ do_get_profile();
+ // Make sure protocols are all loaded.
+ IMServices.core.init();
+ IMServices.core.getProtocols();
+
+ for (const module of ACCOUNT_MODULES) {
+ ok(!Cu.isESModuleLoaded(module), `${module} should be loaded later`);
+ }
+});
diff --git a/comm/chat/components/src/test/test_logger.js b/comm/chat/components/src/test/test_logger.js
new file mode 100644
index 0000000000..be93d8b300
--- /dev/null
+++ b/comm/chat/components/src/test/test_logger.js
@@ -0,0 +1,860 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+var { IMServices } = ChromeUtils.importESModule(
+ "resource:///modules/IMServices.sys.mjs"
+);
+
+const {
+ Logger,
+ gFilePromises,
+ gPendingCleanup,
+ queueFileOperation,
+ getLogFolderPathForAccount,
+ encodeName,
+ getLogFilePathForConversation,
+ getNewLogFileName,
+ appendToFile,
+ getLogWriter,
+ closeLogWriter,
+} = ChromeUtils.importESModule("resource:///modules/logger.sys.mjs");
+
+var logDirPath = PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ "logs"
+);
+
+var dummyAccount = {
+ name: "dummy-account",
+ normalizedName: "dummyaccount",
+ protocol: {
+ normalizedName: "dummy",
+ id: "prpl-dummy",
+ },
+};
+
+var dummyConv = {
+ account: dummyAccount,
+ id: 0,
+ title: "dummy conv",
+ normalizedName: "dummyconv",
+ get name() {
+ return this.normalizedName;
+ },
+ get startDate() {
+ return new Date(2011, 5, 28).valueOf() * 1000;
+ },
+ isChat: false,
+};
+
+// A day after the first one.
+var dummyConv2 = {
+ account: dummyAccount,
+ id: 0,
+ title: "dummy conv",
+ normalizedName: "dummyconv",
+ get name() {
+ return this.normalizedName;
+ },
+ get startDate() {
+ return new Date(2011, 5, 29).valueOf() * 1000;
+ },
+ isChat: false,
+};
+
+var dummyMUC = {
+ account: dummyAccount,
+ id: 1,
+ title: "Dummy MUC",
+ normalizedName: "dummymuc",
+ get name() {
+ return this.normalizedName;
+ },
+ startDate: new Date(2011, 5, 28).valueOf() * 1000,
+ isChat: true,
+};
+
+var encodeName_input = [
+ "CON",
+ "PRN",
+ "AUX",
+ "NUL",
+ "COM3",
+ "LPT5",
+ "file",
+ "file.",
+ "file ",
+ "file_",
+ "file<",
+ "file>",
+ "file:",
+ 'file"',
+ "file/",
+ "file\\",
+ "file|",
+ "file?",
+ "file*",
+ "file&",
+ "file%",
+ "fi<le",
+ "fi>le",
+ "fi:le",
+ 'fi"le',
+ "fi/le",
+ "fi\\le",
+ "fi|le",
+ "fi?le",
+ "fi*le",
+ "fi&le",
+ "fi%le",
+ "<file",
+ ">file",
+ ":file",
+ '"file',
+ "/file",
+ "\\file",
+ "|file",
+ "?file",
+ "*file",
+ "&file",
+ "%file",
+ "\\fi?*&%le<>",
+];
+
+var encodeName_output = [
+ "%CON",
+ "%PRN",
+ "%AUX",
+ "%NUL",
+ "%COM3",
+ "%LPT5",
+ "file",
+ "file._",
+ "file _",
+ "file__",
+ "file%3c",
+ "file%3e",
+ "file%3a",
+ "file%22",
+ "file%2f",
+ "file%5c",
+ "file%7c",
+ "file%3f",
+ "file%2a",
+ "file%26",
+ "file%25",
+ "fi%3cle",
+ "fi%3ele",
+ "fi%3ale",
+ "fi%22le",
+ "fi%2fle",
+ "fi%5cle",
+ "fi%7cle",
+ "fi%3fle",
+ "fi%2ale",
+ "fi%26le",
+ "fi%25le",
+ "%3cfile",
+ "%3efile",
+ "%3afile",
+ "%22file",
+ "%2ffile",
+ "%5cfile",
+ "%7cfile",
+ "%3ffile",
+ "%2afile",
+ "%26file",
+ "%25file",
+ "%5c" + "fi" + "%3f%2a%26%25" + "le" + "%3c%3e", // eslint-disable-line no-useless-concat
+];
+
+var test_queueFileOperation = async function () {
+ let dummyRejectedOperation = () => Promise.reject("Rejected!");
+ let dummyResolvedOperation = () => Promise.resolve("Resolved!");
+
+ // Immediately after calling qFO, "path1" should be mapped to p1.
+ // After yielding, the reference should be cleared from the map.
+ let p1 = queueFileOperation("path1", dummyResolvedOperation);
+ equal(gFilePromises.get("path1"), p1);
+ await p1;
+ ok(!gFilePromises.has("path1"));
+
+ // Repeat above test for a rejected promise.
+ let p2 = queueFileOperation("path2", dummyRejectedOperation);
+ equal(gFilePromises.get("path2"), p2);
+ // This should throw since p2 rejected. Drop the error.
+ await p2.then(
+ () => do_throw(),
+ () => {}
+ );
+ ok(!gFilePromises.has("path2"));
+
+ let onPromiseComplete = (aPromise, aHandler) => {
+ return aPromise.then(aHandler, aHandler);
+ };
+ let test_queueOrder = aOperation => {
+ let promise = queueFileOperation("queueOrderPath", aOperation);
+ let firstOperationComplete = false;
+ onPromiseComplete(promise, () => (firstOperationComplete = true));
+ return queueFileOperation("queueOrderPath", () => {
+ ok(firstOperationComplete);
+ });
+ };
+ // Test the queue order for rejected and resolved promises.
+ await test_queueOrder(dummyResolvedOperation);
+ await test_queueOrder(dummyRejectedOperation);
+};
+
+var test_getLogFolderPathForAccount = async function () {
+ let path = getLogFolderPathForAccount(dummyAccount);
+ equal(
+ PathUtils.join(
+ logDirPath,
+ dummyAccount.protocol.normalizedName,
+ encodeName(dummyAccount.normalizedName)
+ ),
+ path
+ );
+};
+
+// Tests the global function getLogFilePathForConversation in logger.js.
+var test_getLogFilePathForConversation = async function () {
+ let path = getLogFilePathForConversation(dummyConv);
+ let expectedPath = PathUtils.join(
+ logDirPath,
+ dummyAccount.protocol.normalizedName,
+ encodeName(dummyAccount.normalizedName)
+ );
+ expectedPath = PathUtils.join(
+ expectedPath,
+ encodeName(dummyConv.normalizedName)
+ );
+ expectedPath = PathUtils.join(
+ expectedPath,
+ getNewLogFileName(dummyConv.startDate / 1000)
+ );
+ equal(path, expectedPath);
+};
+
+var test_getLogFilePathForMUC = async function () {
+ let path = getLogFilePathForConversation(dummyMUC);
+ let expectedPath = PathUtils.join(
+ logDirPath,
+ dummyAccount.protocol.normalizedName,
+ encodeName(dummyAccount.normalizedName)
+ );
+ expectedPath = PathUtils.join(
+ expectedPath,
+ encodeName(dummyMUC.normalizedName + ".chat")
+ );
+ expectedPath = PathUtils.join(
+ expectedPath,
+ getNewLogFileName(dummyMUC.startDate / 1000)
+ );
+ equal(path, expectedPath);
+};
+
+var test_appendToFile = async function () {
+ const kStringToWrite = "Hello, world!";
+ let path = PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ "testFile.txt"
+ );
+ await IOUtils.write(path, new Uint8Array());
+ appendToFile(path, kStringToWrite);
+ appendToFile(path, kStringToWrite);
+ ok(await queueFileOperation(path, () => IOUtils.exists(path)));
+ let text = await queueFileOperation(path, () => IOUtils.readUTF8(path));
+ // The read text should be equal to kStringToWrite repeated twice.
+ equal(text, kStringToWrite + kStringToWrite);
+ await IOUtils.remove(path);
+};
+
+add_task(async function test_appendToFileHeader() {
+ const kStringToWrite = "Lorem ipsum";
+ let path = PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ "headerTestFile.txt"
+ );
+ await appendToFile(path, kStringToWrite, true);
+ await appendToFile(path, kStringToWrite, true);
+ let text = await queueFileOperation(path, () => IOUtils.readUTF8(path));
+ // The read text should be equal to kStringToWrite once, since the second
+ // create should just noop.
+ equal(text, kStringToWrite);
+ await IOUtils.remove(path);
+});
+
+// Tests the getLogPathsForConversation API defined in the imILogger interface.
+var test_getLogPathsForConversation = async function () {
+ let logger = new Logger();
+ let paths = await logger.getLogPathsForConversation(dummyConv);
+ // The path should be null since a LogWriter hasn't been created yet.
+ equal(paths, null);
+ let logWriter = getLogWriter(dummyConv);
+ paths = await logger.getLogPathsForConversation(dummyConv);
+ equal(paths.length, 1);
+ equal(paths[0], logWriter.currentPath);
+ ok(await IOUtils.exists(paths[0]));
+ // Ensure this doesn't interfere with future tests.
+ await IOUtils.remove(paths[0]);
+ closeLogWriter(dummyConv);
+};
+
+var test_logging = async function () {
+ let logger = new Logger();
+ let oneSec = 1000000; // Microseconds.
+
+ // Creates a set of dummy messages for a conv (sets appropriate times).
+ let getMsgsForConv = function (aConv) {
+ // Convert to seconds because that's what logMessage expects.
+ let startTime = Math.round(aConv.startDate / oneSec);
+ return [
+ {
+ time: startTime + 1,
+ who: "personA",
+ displayMessage: "Hi!",
+ outgoing: true,
+ },
+ {
+ time: startTime + 2,
+ who: "personB",
+ displayMessage: "Hello!",
+ incoming: true,
+ },
+ {
+ time: startTime + 3,
+ who: "personA",
+ displayMessage: "What's up?",
+ outgoing: true,
+ },
+ {
+ time: startTime + 4,
+ who: "personB",
+ displayMessage: "Nothing much!",
+ incoming: true,
+ },
+ {
+ time: startTime + 5,
+ who: "personB",
+ displayMessage: "Encrypted msg",
+ remoteId: "identifier",
+ incoming: true,
+ isEncrypted: true,
+ },
+ {
+ time: startTime + 6,
+ who: "personA",
+ displayMessage: "Deleted",
+ remoteId: "otherID",
+ outgoing: true,
+ isEncrypted: true,
+ deleted: true,
+ },
+ ];
+ };
+ let firstDayMsgs = getMsgsForConv(dummyConv);
+ let secondDayMsgs = getMsgsForConv(dummyConv2);
+
+ let logMessagesForConv = async function (aConv, aMessages) {
+ let logWriter = getLogWriter(aConv);
+ for (let message of aMessages) {
+ logWriter.logMessage(message);
+ }
+ // If we don't wait for the messages to get written, we have no guarantee
+ // later in the test that the log files were created, and getConversation
+ // will return an EmptyEnumerator. Logging the messages is queued on the
+ // _initialized promise, so we need to await on that first.
+ await logWriter._initialized;
+ await gFilePromises.get(logWriter.currentPath);
+ // Ensure two different files for the different dates.
+ closeLogWriter(aConv);
+ };
+ await logMessagesForConv(dummyConv, firstDayMsgs);
+ await logMessagesForConv(dummyConv2, secondDayMsgs);
+
+ // Write a zero-length file and a file with incorrect JSON for each day
+ // to ensure they are handled correctly.
+ let logDir = PathUtils.parent(getLogFilePathForConversation(dummyConv));
+ let createBadFiles = async function (aConv) {
+ let blankFile = PathUtils.join(
+ logDir,
+ getNewLogFileName((aConv.startDate + oneSec) / 1000)
+ );
+ let invalidJSONFile = PathUtils.join(
+ logDir,
+ getNewLogFileName((aConv.startDate + 2 * oneSec) / 1000)
+ );
+ await IOUtils.write(blankFile, new Uint8Array());
+ await IOUtils.writeUTF8(invalidJSONFile, "This isn't JSON!");
+ };
+ await createBadFiles(dummyConv);
+ await createBadFiles(dummyConv2);
+
+ let testMsgs = function (aMsgs, aExpectedMsgs, aExpectedSessions) {
+ // Ensure the number of session messages is correct.
+ let sessions = aMsgs.filter(aMsg => aMsg.who == "sessionstart").length;
+ equal(sessions, aExpectedSessions);
+
+ // Discard session messages, etc.
+ aMsgs = aMsgs.filter(aMsg => !aMsg.noLog);
+
+ equal(aMsgs.length, aExpectedMsgs.length);
+
+ for (let i = 0; i < aMsgs.length; ++i) {
+ let message = aMsgs[i],
+ expectedMessage = aExpectedMsgs[i];
+ for (let prop in expectedMessage) {
+ ok(prop in message);
+ equal(expectedMessage[prop], message[prop]);
+ }
+ }
+ };
+
+ // Accepts time in seconds, reduces it to a date, and returns the value in millis.
+ let reduceTimeToDate = function (aTime) {
+ let date = new Date(aTime * 1000);
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ return date.valueOf();
+ };
+
+ // Group expected messages by day.
+ let messagesByDay = new Map();
+ messagesByDay.set(
+ reduceTimeToDate(firstDayMsgs[0].time),
+ firstDayMsgs.filter(msg => !msg.deleted)
+ );
+ messagesByDay.set(
+ reduceTimeToDate(secondDayMsgs[0].time),
+ secondDayMsgs.filter(msg => !msg.deleted)
+ );
+
+ let logs = await logger.getLogsForConversation(dummyConv);
+ for (let log of logs) {
+ let conv = await log.getConversation();
+ let date = reduceTimeToDate(log.time);
+ // 3 session messages - for daily logs, bad files are included.
+ testMsgs(conv.getMessages(), messagesByDay.get(date), 3);
+ }
+
+ // Remove the created log files, testing forEach in the process.
+ await logger.forEach({
+ async processLog(aLog) {
+ let info = await IOUtils.stat(aLog);
+ notEqual(info.type, "directory");
+ ok(aLog.endsWith(".json"));
+ await IOUtils.remove(aLog);
+ },
+ });
+ let logFolder = PathUtils.parent(getLogFilePathForConversation(dummyConv));
+ // The folder should now be empty - this will throw if it isn't.
+ await IOUtils.remove(logFolder, { ignoreAbsent: false });
+};
+
+var test_logFileSplitting = async function () {
+ // Start clean, remove the log directory.
+ await IOUtils.remove(logDirPath, { recursive: true });
+ let logWriter = getLogWriter(dummyConv);
+ let startTime = logWriter._startTime / 1000; // Message times are in seconds.
+ let oldPath = logWriter.currentPath;
+ let message = {
+ time: startTime,
+ who: "John Doe",
+ originalMessage: "Hello, world!",
+ outgoing: true,
+ };
+
+ let logMessage = async function (aMessage) {
+ logWriter.logMessage(aMessage);
+ await logWriter._initialized;
+ await gFilePromises.get(logWriter.currentPath);
+ };
+
+ await logMessage(message);
+ message.time += logWriter.kInactivityLimit / 1000 + 1;
+ // This should go in a new log file.
+ await logMessage(message);
+ notEqual(logWriter.currentPath, oldPath);
+ // The log writer's new start time should be the time of the message.
+ equal(message.time * 1000, logWriter._startTime);
+
+ let getCurrentHeader = async function () {
+ return JSON.parse(
+ (await IOUtils.readUTF8(logWriter.currentPath)).split("\n")[0]
+ );
+ };
+
+ // The header of the new log file should not have the continuedSession flag set.
+ ok(!(await getCurrentHeader()).continuedSession);
+
+ // Set the start time sufficiently before midnight, and the last message time
+ // to just before midnight. A new log file should be created at midnight.
+ logWriter._startTime = new Date(logWriter._startTime).setHours(
+ 24,
+ 0,
+ 0,
+ -(logWriter.kDayOverlapLimit + 1)
+ );
+ let nearlyMidnight = new Date(logWriter._startTime).setHours(24, 0, 0, -1);
+ oldPath = logWriter.currentPath;
+ logWriter._lastMessageTime = nearlyMidnight;
+ message.time = new Date(nearlyMidnight).setHours(24, 0, 0, 1) / 1000;
+ await logMessage(message);
+ // The message should have gone in a new file.
+ notEqual(oldPath, logWriter.currentPath);
+ // The header should have the continuedSession flag set this time.
+ ok((await getCurrentHeader()).continuedSession);
+
+ // Ensure a new file is created every kMessageCountLimit messages.
+ oldPath = logWriter.currentPath;
+ let messageCountLimit = logWriter.kMessageCountLimit;
+ for (let i = 0; i < messageCountLimit; ++i) {
+ logMessage(message);
+ }
+ await logMessage(message);
+ notEqual(oldPath, logWriter.currentPath);
+ // The header should have the continuedSession flag set this time too.
+ ok((await getCurrentHeader()).continuedSession);
+ // Again, to make sure it still works correctly after splitting it once already.
+ oldPath = logWriter.currentPath;
+ // We already logged one message to ensure it went into a new file, so i = 1.
+ for (let i = 1; i < messageCountLimit; ++i) {
+ logMessage(message);
+ }
+ await logMessage(message);
+ notEqual(oldPath, logWriter.currentPath);
+ ok((await getCurrentHeader()).continuedSession);
+
+ // The new start time is the time of the message. If we log sufficiently more
+ // messages with the same time property, ensure that the start time of the next
+ // log file is greater than the previous one, and that a new path is being used.
+ let oldStartTime = logWriter._startTime;
+ oldPath = logWriter.currentPath;
+ logWriter._messageCount = messageCountLimit;
+ await logMessage(message);
+ notEqual(oldPath, logWriter.currentPath);
+ ok(logWriter._startTime > oldStartTime);
+
+ // Do it again with the same message.
+ oldStartTime = logWriter._startTime;
+ oldPath = logWriter.currentPath;
+ logWriter._messageCount = messageCountLimit;
+ await logMessage(message);
+ notEqual(oldPath, logWriter.currentPath);
+ ok(logWriter._startTime > oldStartTime);
+
+ // Clean up.
+ await IOUtils.remove(logDirPath, { recursive: true });
+ closeLogWriter(dummyConv);
+};
+
+add_task(async function test_logWithEdits() {
+ // Start clean, remove the log directory.
+ await IOUtils.remove(logDirPath, { recursive: true });
+ let logger = new Logger();
+ let logFilePath = getLogFilePathForConversation(dummyConv);
+ await IOUtils.writeUTF8(
+ logFilePath,
+ [
+ {
+ date: "2022-03-04T12:00:03.508Z",
+ name: "test",
+ title: "test",
+ account: "@test:example.com",
+ protocol: "matrix",
+ isChat: false,
+ normalizedName: "!foobar:example.com",
+ },
+ {
+ date: "2022-03-04T11:59:48.000Z",
+ who: "@other:example.com",
+ text: "Decrypting...",
+ flags: ["incoming", "delayed", "isEncrypted"],
+ remoteId: "$AjmS57jkBbYnSnC01r3fXya8BfuHIMAw9mOYQRlnkFk",
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:51.000Z",
+ who: "@other:example.com",
+ text: "Decrypting...",
+ flags: ["incoming", "delayed", "isEncrypted"],
+ remoteId: "$00zdmKvErkDR4wMaxZBCFsV1WwqPQRolP0kYiXPIXsQ",
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:53.000Z",
+ who: "@other:example.com",
+ text: "Decrypting...",
+ flags: ["incoming", "delayed", "isEncrypted"],
+ remoteId: "$Z6ILSf7cBMRbr_B6Z6DPHJWzf-Utxa8_s0f6vxhR_VQ",
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:56.000Z",
+ who: "@other:example.com",
+ text: "Decrypting...",
+ flags: ["incoming", "delayed", "isEncrypted"],
+ remoteId: "$GFlcel-9tWrTvSb7HM_113-WpkzEdB4neglPVpZn3dM",
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:56.000Z",
+ who: "@other:example.com",
+ text: "Lorem ipsum dolor sit amet",
+ flags: ["incoming", "isEncrypted"],
+ remoteId: "$GFlcel-9tWrTvSb7HM_113-WpkzEdB4neglPVpZn3dM",
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:53.000Z",
+ who: "@other:example.com",
+ text: "consectetur adipiscing elit",
+ flags: ["incoming", "isEncrypted"],
+ remoteId: "$Z6ILSf7cBMRbr_B6Z6DPHJWzf-Utxa8_s0f6vxhR_VQ",
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:51.000Z",
+ who: "@other:example.com",
+ text: "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
+ flags: ["incoming", "isEncrypted"],
+ remoteId: "$00zdmKvErkDR4wMaxZBCFsV1WwqPQRolP0kYiXPIXsQ",
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:48.000Z",
+ who: "@other:example.com",
+ text: "Ut enim ad minim veniam",
+ flags: ["incoming", "isEncrypted"],
+ remoteId: "$AjmS57jkBbYnSnC01r3fXya8BfuHIMAw9mOYQRlnkFk",
+ alias: "other",
+ },
+ ]
+ .map(message => JSON.stringify(message))
+ .join("\n"),
+ {
+ mode: "create",
+ }
+ );
+ let logs = await logger.getLogsForConversation(dummyConv);
+ equal(logs.length, 1);
+ const conv = await logs[0].getConversation();
+ const messages = conv.getMessages();
+ equal(messages.length, 5);
+ for (const msg of messages) {
+ if (msg.who !== "sessionstart") {
+ notEqual(msg.displayMessage, "Decrypting...");
+ }
+ }
+
+ // Clean up.
+ await IOUtils.remove(logDirPath, { recursive: true });
+});
+
+// Ensure that any message with a remoteId that has a deleted flag in the
+// latest version is not visible in logs.
+add_task(async function test_logWithDeletedMessages() {
+ // Start clean, remove the log directory.
+ await IOUtils.remove(logDirPath, { recursive: true });
+ let logger = new Logger();
+ let logFilePath = getLogFilePathForConversation(dummyConv);
+ const remoteId = "$GFlcel-9tWrTvSb7HM_113-WpkzEdB4neglPVpZn3dM";
+ await IOUtils.writeUTF8(
+ logFilePath,
+ [
+ {
+ date: "2022-03-04T12:00:03.508Z",
+ name: "test",
+ title: "test",
+ account: "@test:example.com",
+ protocol: "matrix",
+ isChat: false,
+ normalizedName: "!foobar:example.com",
+ },
+ {
+ date: "2022-03-04T11:59:56.000Z",
+ who: "@other:example.com",
+ text: "Decrypting...",
+ flags: ["incoming", "isEncrypted"],
+ remoteId,
+ alias: "other",
+ },
+ {
+ date: "2022-03-04T11:59:56.000Z",
+ who: "@other:example.com",
+ text: "Message was redacted.",
+ flags: ["incoming", "isEncrypted", "deleted"],
+ remoteId,
+ alias: "other",
+ },
+ ]
+ .map(message => JSON.stringify(message))
+ .join("\n"),
+ {
+ mode: "create",
+ }
+ );
+ let logs = await logger.getLogsForConversation(dummyConv);
+ equal(logs.length, 1);
+ const conv = await logs[0].getConversation();
+ const messages = conv.getMessages();
+ equal(messages.length, 1);
+ equal(messages[0].who, "sessionstart");
+
+ // Clean up.
+ await IOUtils.remove(logDirPath, { recursive: true });
+});
+
+add_task(async function test_logDeletedMessageCleanup() {
+ // Start clean, remove the log directory.
+ await IOUtils.remove(logDirPath, { recursive: true });
+ let logger = new Logger();
+ let logWriter = getLogWriter(dummyConv);
+ let remoteId = "testId";
+
+ let logMessage = async function (aMessage) {
+ logWriter.logMessage(aMessage);
+ await logWriter._initialized;
+ await gFilePromises.get(logWriter.currentPath);
+ };
+
+ await logMessage({
+ time: Math.floor(dummyConv.startDate / 1000000) + 10,
+ who: "test",
+ displayMessage: "delete me",
+ remoteId,
+ incoming: true,
+ });
+
+ await logMessage({
+ time: Math.floor(dummyConv.startDate / 1000000) + 20,
+ who: "test",
+ displayMessage: "Message is deleted",
+ remoteId,
+ deleted: true,
+ incoming: true,
+ });
+ ok(gPendingCleanup.has(logWriter.currentPath));
+ equal(
+ Services.prefs.getStringPref("chat.logging.cleanup.pending"),
+ JSON.stringify([logWriter.currentPath])
+ );
+
+ await new Promise(resolve => ChromeUtils.idleDispatch(resolve));
+ await (gFilePromises.get(logWriter.currentPath) || Promise.resolve());
+
+ ok(!gPendingCleanup.has(logWriter.currentPath));
+ equal(Services.prefs.getStringPref("chat.logging.cleanup.pending"), "[]");
+
+ let logs = await logger.getLogsForConversation(dummyConv);
+ equal(logs.length, 1, "Only a single log file for this conversation");
+ let conv = await logs[0].getConversation();
+ let messages = conv.getMessages();
+ equal(messages.length, 1, "Only the log header is left");
+ equal(messages[0].who, "sessionstart");
+
+ // Check that the message contents were removed from the file on disk. The
+ // log parser above removes it either way.
+ let logOnDisk = await IOUtils.readUTF8(logWriter.currentPath);
+ let rawMessages = logOnDisk
+ .split("\n")
+ .filter(Boolean)
+ .map(line => JSON.parse(line));
+ equal(rawMessages.length, 3);
+ equal(rawMessages[1].text, "", "Deleted message content was removed");
+ equal(
+ rawMessages[2].text,
+ "Message is deleted",
+ "Deletion content is unaffected"
+ );
+
+ // Clean up.
+ await IOUtils.remove(logDirPath, { recursive: true });
+
+ closeLogWriter(dummyConv);
+});
+
+add_task(async function test_displayOldActionLog() {
+ // Start clean, remove the log directory.
+ await IOUtils.remove(logDirPath, { recursive: true });
+ let logger = new Logger();
+ let logFilePath = getLogFilePathForConversation(dummyConv);
+ await IOUtils.writeUTF8(
+ logFilePath,
+ [
+ {
+ date: "2022-03-04T12:00:03.508Z",
+ name: "test",
+ title: "test",
+ account: "@test:example.com",
+ protocol: "matrix",
+ isChat: false,
+ normalizedName: "!foobar:example.com",
+ },
+ {
+ date: "2022-03-04T11:59:56.000Z",
+ who: "@other:example.com",
+ text: "/me an old action",
+ flags: ["incoming"],
+ },
+ {
+ date: "2022-03-04T11:59:56.000Z",
+ who: "@other:example.com",
+ text: "a new action",
+ flags: ["incoming", "action"],
+ },
+ ]
+ .map(message => JSON.stringify(message))
+ .join("\n"),
+ {
+ mode: "create",
+ }
+ );
+ let logs = await logger.getLogsForConversation(dummyConv);
+ equal(logs.length, 1);
+ for (let log of logs) {
+ const conv = await log.getConversation();
+ const messages = conv.getMessages();
+ equal(messages.length, 3);
+ for (let message of messages) {
+ if (message.who !== "sessionstart") {
+ ok(message.action, "Message is marked as action");
+ ok(
+ !message.displayMessage.startsWith("/me"),
+ "Message has no leading /me"
+ );
+ }
+ }
+ }
+
+ // Clean up.
+ await IOUtils.remove(logDirPath, { recursive: true });
+});
+
+add_task(function test_encodeName() {
+ // Test encodeName().
+ for (let i = 0; i < encodeName_input.length; ++i) {
+ equal(encodeName(encodeName_input[i]), encodeName_output[i]);
+ }
+});
+
+add_task(test_getLogFolderPathForAccount);
+
+add_task(test_getLogFilePathForConversation);
+
+add_task(test_getLogFilePathForMUC);
+
+add_task(test_queueFileOperation);
+
+add_task(test_appendToFile);
+
+add_task(test_getLogPathsForConversation);
+
+add_task(test_logging);
+
+add_task(test_logFileSplitting);
diff --git a/comm/chat/components/src/test/xpcshell.ini b/comm/chat/components/src/test/xpcshell.ini
new file mode 100644
index 0000000000..63cce6e7e1
--- /dev/null
+++ b/comm/chat/components/src/test/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head =
+tail =
+
+[test_accounts.js]
+[test_commands.js]
+[test_conversations.js]
+[test_init.js]
+[test_logger.js]