diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/chat/components/src/test | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/chat/components/src/test')
-rw-r--r-- | comm/chat/components/src/test/test_accounts.js | 48 | ||||
-rw-r--r-- | comm/chat/components/src/test/test_commands.js | 271 | ||||
-rw-r--r-- | comm/chat/components/src/test/test_conversations.js | 239 | ||||
-rw-r--r-- | comm/chat/components/src/test/test_init.js | 28 | ||||
-rw-r--r-- | comm/chat/components/src/test/test_logger.js | 860 | ||||
-rw-r--r-- | comm/chat/components/src/test/xpcshell.ini | 9 |
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] |