diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/db/gloda/test/unit/base_index_messages.js | 1461 |
1 files changed, 1461 insertions, 0 deletions
diff --git a/comm/mailnews/db/gloda/test/unit/base_index_messages.js b/comm/mailnews/db/gloda/test/unit/base_index_messages.js new file mode 100644 index 0000000000..bea2337d7f --- /dev/null +++ b/comm/mailnews/db/gloda/test/unit/base_index_messages.js @@ -0,0 +1,1461 @@ +/* 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/. */ + +/* + * This file tests our indexing prowess. This includes both our ability to + * properly be triggered by events taking place in thunderbird as well as our + * ability to correctly extract/index the right data. + * In general, if these tests pass, things are probably working quite well. + * + * This test has local, IMAP online, IMAP offline, and IMAP online-become-offline + * variants. See the text_index_messages_*.js files. + * + * Things we don't test that you think we might test: + * - Full-text search. Happens in query testing. + */ + +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm"); +var { GlodaConstants } = ChromeUtils.import( + "resource:///modules/gloda/GlodaConstants.jsm" +); +var { GlodaMsgIndexer } = ChromeUtils.import( + "resource:///modules/gloda/IndexMsg.jsm" +); +var { GlodaIndexer } = ChromeUtils.import( + "resource:///modules/gloda/GlodaIndexer.jsm" +); +var { queryExpect, sqlExpectCount } = ChromeUtils.import( + "resource://testing-common/gloda/GlodaQueryHelper.jsm" +); +var { + assertExpectedMessagesIndexed, + waitForGlodaIndexer, + nukeGlodaCachesAndCollections, +} = ChromeUtils.import("resource://testing-common/gloda/GlodaTestHelper.jsm"); +var { + configureGlodaIndexing, + waitForGlodaDBFlush, + waitForIndexingHang, + resumeFromSimulatedHang, + permuteMessages, + makeABCardForAddressPair, +} = ChromeUtils.import( + "resource://testing-common/gloda/GlodaTestHelperFunctions.jsm" +); +var { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); +var { MessageInjection } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageInjection.jsm" +); +var { SyntheticMessageSet, SyntheticPartMultiMixed, SyntheticPartLeaf } = + ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm"); +var { TagNoun } = ChromeUtils.import("resource:///modules/gloda/NounTag.jsm"); + +// Whether we can expect fulltext results +var expectFulltextResults = true; + +/** + * Should we force our folders offline after we have indexed them once. We do + * this in the online_to_offline test variant. + */ +var goOffline = false; + +var messageInjection; +var msgGen; +var scenarios; + +/* ===== Indexing Basics ===== */ + +/** + * Index a message, wait for a commit, make sure the header gets the property + * set correctly. Then modify the message, verify the dirty property shows + * up, flush again, and make sure the dirty property goes clean again. + */ +async function test_pending_commit_tracker_flushes_correctly() { + let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + + // Before the flush, there should be no gloda-id property. + let msgHdr = msgSet.getMsgHdr(0); + // Get it as a string to make sure it's empty rather than possessing a value. + Assert.equal(msgHdr.getStringProperty("gloda-id"), ""); + + await waitForGlodaDBFlush(); + + // After the flush there should be a gloda-id property and it should + // equal the gloda id. + let gmsg = msgSet.glodaMessages[0]; + Assert.equal(msgHdr.getUint32Property("gloda-id"), gmsg.id); + + // Make sure no dirty property was written. + Assert.equal(msgHdr.getStringProperty("gloda-dirty"), ""); + + // Modify the message. + msgSet.setRead(true); + await waitForGlodaIndexer(msgSet); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + + // Now there should be a dirty property and it should be 1. + Assert.equal( + msgHdr.getUint32Property("gloda-dirty"), + GlodaMsgIndexer.kMessageDirty + ); + + // Flush. + await waitForGlodaDBFlush(); + + // Now dirty should be 0 and the gloda id should still be the same. + Assert.equal( + msgHdr.getUint32Property("gloda-dirty"), + GlodaMsgIndexer.kMessageClean + ); + Assert.equal(msgHdr.getUint32Property("gloda-id"), gmsg.id); +} + +/** + * Make sure that PendingCommitTracker causes a msgdb commit to occur so that + * if the nsIMsgFolder's msgDatabase attribute has already been nulled + * (which is normally how we force a msgdb commit), that the changes to the + * header actually hit the disk. + */ +async function test_pending_commit_causes_msgdb_commit() { + // New message, index it. + let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + + // Force the msgDatabase closed; the sqlite commit will not yet have occurred. + messageInjection.getRealInjectionFolder(folder).msgDatabase = null; + // Make the commit happen, this causes the header to get set. + await waitForGlodaDBFlush(); + + // Force a GC. This will kill off the header and the database, losing data + // if we are not protecting it. + Cu.forceGC(); + + // Now retrieve the header and make sure it has the gloda id set! + let msgHdr = msgSet.getMsgHdr(0); + Assert.equal( + msgHdr.getUint32Property("gloda-id"), + msgSet.glodaMessages[0].id + ); +} + +/** + * Give the indexing sweep a workout. + * + * This includes: + * - Basic indexing sweep across never-before-indexed folders. + * - Indexing sweep across folders with just some changes. + * - Filthy pass. + */ +async function test_indexing_sweep() { + // -- Never-before-indexed folders. + // Turn off event-driven indexing. + configureGlodaIndexing({ event: false }); + + let [[folderA], setA1, setA2] = await messageInjection.makeFoldersWithSets( + 1, + [{ count: 3 }, { count: 2 }] + ); + let [, setB1, setB2] = await messageInjection.makeFoldersWithSets(1, [ + { count: 3 }, + { count: 2 }, + ]); + let [[folderC], setC1, setC2] = await messageInjection.makeFoldersWithSets( + 1, + [{ count: 3 }, { count: 2 }] + ); + + // Make sure that event-driven job gets nuked out of existence + GlodaIndexer.purgeJobsUsingFilter(() => true); + + // Turn on event-driven indexing again; this will trigger a sweep. + configureGlodaIndexing({ event: true }); + GlodaMsgIndexer.indexingSweepNeeded = true; + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([setA1, setA2, setB1, setB2, setC1, setC2]) + ); + + // -- Folders with some changes, pending commits. + // Indexing off. + configureGlodaIndexing({ event: false }); + + setA1.setRead(true); + setB2.setRead(true); + + // Indexing on, killing all outstanding jobs, trigger sweep. + GlodaIndexer.purgeJobsUsingFilter(() => true); + configureGlodaIndexing({ event: true }); + GlodaMsgIndexer.indexingSweepNeeded = true; + + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([setA1, setB2])); + + // -- Folders with some changes, no pending commits. + // Force a commit to clear out our pending commits. + await waitForGlodaDBFlush(); + // Indexing off. + configureGlodaIndexing({ event: false }); + + setA2.setRead(true); + setB1.setRead(true); + + // Indexing on, killing all outstanding jobs, trigger sweep. + GlodaIndexer.purgeJobsUsingFilter(() => true); + configureGlodaIndexing({ event: true }); + GlodaMsgIndexer.indexingSweepNeeded = true; + + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([setA2, setB1])); + + // -- Filthy foldering indexing. + // Just mark the folder filthy and make sure that we reindex everyone. + // IMPORTANT! The trick of marking the folder filthy only works because + // we flushed/committed the database above; the PendingCommitTracker + // is not aware of bogus filthy-marking of folders. + // We leave the verification of the implementation details to + // test_index_sweep_folder.js. + let glodaFolderC = Gloda.getFolderForFolder( + messageInjection.getRealInjectionFolder(folderC) + ); + // Marked gloda folder dirty. + glodaFolderC._dirtyStatus = glodaFolderC.kFolderFilthy; + GlodaMsgIndexer.indexingSweepNeeded = true; + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([setC1, setC2])); + + // -- Forced folder indexing. + var callbackInvoked = false; + GlodaMsgIndexer.indexFolder( + messageInjection.getRealInjectionFolder(folderA), + { + force: true, + callback() { + callbackInvoked = true; + }, + } + ); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([setA1, setA2])); + Assert.ok(callbackInvoked); +} + +/** + * We used to screw up and downgrade filthy folders to dirty if we saw an event + * happen in the folder before we got to the folder; this tests that we no + * longer do that. + */ +async function test_event_driven_indexing_does_not_mess_with_filthy_folders() { + // Add a folder with a message. + let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + + // Fake marking the folder filthy. + let glodaFolder = Gloda.getFolderForFolder( + messageInjection.getRealInjectionFolder(folder) + ); + glodaFolder._dirtyStatus = glodaFolder.kFolderFilthy; + + // Generate an event in the folder. + msgSet.setRead(true); + // Make sure the indexer did not do anything and the folder is still filthy. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); + Assert.equal(glodaFolder._dirtyStatus, glodaFolder.kFolderFilthy); + // Also, the message should not have actually gotten marked dirty. + Assert.equal(msgSet.getMsgHdr(0).getUint32Property("gloda-dirty"), 0); + + // Let's make the message un-read again for consistency with the gloda state. + msgSet.setRead(false); + // Make the folder dirty and let an indexing sweep take care of this so we + // don't get extra events in subsequent tests. + glodaFolder._dirtyStatus = glodaFolder.kFolderDirty; + GlodaMsgIndexer.indexingSweepNeeded = true; + // The message won't get indexed though. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); +} + +async function test_indexing_never_priority() { + // Add a folder with a bunch of messages. + let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + + // Index it, and augment the msgSet with the glodaMessages array + // for later use by sqlExpectCount. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + + // Explicitly tell gloda to never index this folder. + let XPCOMFolder = messageInjection.getRealInjectionFolder(folder); + let glodaFolder = Gloda.getFolderForFolder(XPCOMFolder); + GlodaMsgIndexer.setFolderIndexingPriority( + XPCOMFolder, + glodaFolder.kIndexingNeverPriority + ); + + // Verify that the setter and getter do the right thing. + Assert.equal( + glodaFolder.indexingPriority, + glodaFolder.kIndexingNeverPriority + ); + + // Check that existing message is marked as deleted. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] })); + + // Make sure the deletion hit the database. + await sqlExpectCount( + 1, + "SELECT COUNT(*) from folderLocations WHERE id = ? AND indexingPriority = ?", + glodaFolder.id, + glodaFolder.kIndexingNeverPriority + ); + + // Add another message. + await messageInjection.makeNewSetsInFolders([folder], [{ count: 1 }]); + + // Make sure that indexing returns nothing. + GlodaMsgIndexer.indexingSweepNeeded = true; + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); +} + +async function test_setting_indexing_priority_never_while_indexing() { + if (!messageInjection.messageInjectionIsLocal()) { + return; + } + + // Configure the gloda indexer to hang while streaming the message. + configureGlodaIndexing({ hangWhile: "streaming" }); + + // Create a folder with a message inside. + let [[folder]] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + + await waitForIndexingHang(); + + // Explicitly tell gloda to never index this folder. + let XPCOMFolder = messageInjection.getRealInjectionFolder(folder); + let glodaFolder = Gloda.getFolderForFolder(XPCOMFolder); + GlodaMsgIndexer.setFolderIndexingPriority( + XPCOMFolder, + glodaFolder.kIndexingNeverPriority + ); + + // Reset indexing to not hang. + configureGlodaIndexing({}); + + // Sorta get the event chain going again. + await resumeFromSimulatedHang(true); + + // Because the folder was dirty it should actually end up getting indexed, + // so in the end the message will get indexed. Also, make sure a cleanup + // was observed. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([], { cleanedUp: 1 })); +} + +/* ===== Threading / Conversation Grouping ===== */ + +var gSynMessages = []; +function allMessageInSameConversation(aSynthMessage, aGlodaMessage, aConvID) { + if (aConvID === undefined) { + return aGlodaMessage.conversationID; + } + Assert.equal(aConvID, aGlodaMessage.conversationID); + // Cheat and stash the synthetic message (we need them for one of the IMAP + // tests). + gSynMessages.push(aSynthMessage); + return aConvID; +} + +/** + * Test our conversation/threading logic in the straight-forward direct + * reply case, the missing intermediary case, and the siblings with missing + * parent case. We also test all permutations of receipt of those messages. + * (Also tests that we index new messages.) + */ +async function test_threading_direct_reply() { + let permutationMessages = await permuteMessages( + scenarios.directReply, + messageInjection + ); + for (const preparedMessage of permutationMessages) { + let message = await preparedMessage(); + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([message], allMessageInSameConversation) + ); + } +} + +async function test_threading_missing_intermediary() { + let permutationMessages = await permuteMessages( + scenarios.missingIntermediary, + messageInjection + ); + for (const preparedMessage of permutationMessages) { + let message = await preparedMessage(); + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([message], allMessageInSameConversation) + ); + } +} +async function test_threading_siblings_missing_parent() { + let permutationMessages = await permuteMessages( + scenarios.siblingsMissingParent, + messageInjection + ); + for (const preparedMessage of permutationMessages) { + let message = await preparedMessage(); + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([message], allMessageInSameConversation) + ); + } +} + +/** + * Test the bit that says "if we're fulltext-indexing the message and we + * discover it didn't have any attachments, clear the attachment bit from the + * message header". + */ +async function test_attachment_flag() { + // Create a synthetic message with an attachment that won't normally be listed + // in the attachment pane (Content-Disposition: inline, no filename, and + // displayable inline). + let smsg = msgGen.makeMessage({ + name: "test message with part 1.2 attachment", + attachments: [ + { + body: "attachment", + filename: "", + format: "", + }, + ], + }); + // Save it off for test_attributes_fundamental_from_disk. + let msgSet = new SyntheticMessageSet([smsg]); + let folder = (fundamentalFolderHandle = + await messageInjection.makeEmptyFolder()); + await messageInjection.addSetsToFolders([folder], [msgSet]); + + // If we need to go offline, let the indexing pass run, then force us offline. + if (goOffline) { + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + await messageInjection.makeFolderAndContentsOffline(folder); + // Now the next indexer wait will wait for the next indexing pass. + } + + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([msgSet], { + verifier: verify_attachment_flag, + }) + ); +} + +function verify_attachment_flag(smsg, gmsg) { + // -- Attachments. We won't have these if we don't have fulltext results. + if (expectFulltextResults) { + Assert.equal(gmsg.attachmentNames.length, 0); + Assert.equal(gmsg.attachmentInfos.length, 0); + Assert.equal( + false, + gmsg.folderMessage.flags & Ci.nsMsgMessageFlags.Attachment + ); + } +} +/* ===== Fundamental Attributes (per GlodaFundAttr.jsm) ===== */ + +/** + * Save the synthetic message created in test_attributes_fundamental for the + * benefit of test_attributes_fundamental_from_disk. + */ +var fundamentalSyntheticMessage; +var fundamentalFolderHandle; +/** + * We're saving this one so that we can move the message later and verify that + * the attributes are consistent. + */ +var fundamentalMsgSet; +var fundamentalGlodaMsgAttachmentUrls; +/** + * Save the resulting gloda message id corresponding to the + * fundamentalSyntheticMessage so we can use it to query the message from disk. + */ +var fundamentalGlodaMessageId; + +/** + * Test that we extract the 'fundamental attributes' of a message properly + * 'Fundamental' in this case is talking about the attributes defined/extracted + * by gloda's GlodaFundAttr.jsm and perhaps the core message indexing logic itself + * (which show up as kSpecial* attributes in GlodaFundAttr.jsm anyways.) + */ +async function test_attributes_fundamental() { + // Create a synthetic message with attachment. + let smsg = msgGen.makeMessage({ + name: "test message", + bodyPart: new SyntheticPartMultiMixed([ + new SyntheticPartLeaf({ body: "I like cheese!" }), + msgGen.makeMessage({ body: { body: "I like wine!" } }), // That's one attachment. + ]), + attachments: [ + { filename: "bob.txt", body: "I like bread!" }, // And that's another one. + ], + }); + // Save it off for test_attributes_fundamental_from_disk. + fundamentalSyntheticMessage = smsg; + let msgSet = new SyntheticMessageSet([smsg]); + fundamentalMsgSet = msgSet; + let folder = (fundamentalFolderHandle = + await messageInjection.makeEmptyFolder()); + await messageInjection.addSetsToFolders([folder], [msgSet]); + + // If we need to go offline, let the indexing pass run, then force us offline. + if (goOffline) { + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + await messageInjection.makeFolderAndContentsOffline(folder); + // Now the next indexer wait will wait for the next indexing pass. + } + + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([msgSet], { + verifier: verify_attributes_fundamental, + }) + ); +} + +function verify_attributes_fundamental(smsg, gmsg) { + // Save off the message id for test_attributes_fundamental_from_disk. + fundamentalGlodaMessageId = gmsg.id; + if (gmsg.attachmentInfos) { + fundamentalGlodaMsgAttachmentUrls = gmsg.attachmentInfos.map( + att => att.url + ); + } else { + fundamentalGlodaMsgAttachmentUrls = []; + } + + Assert.equal( + gmsg.folderURI, + messageInjection.getRealInjectionFolder(fundamentalFolderHandle).URI + ); + + // -- Subject + Assert.equal(smsg.subject, gmsg.conversation.subject); + Assert.equal(smsg.subject, gmsg.subject); + + // -- Contact/identity information. + // - From + // Check the e-mail address. + Assert.equal(gmsg.from.kind, "email"); + Assert.equal(smsg.fromAddress, gmsg.from.value); + // Check the name. + Assert.equal(smsg.fromName, gmsg.from.contact.name); + + // - To + Assert.equal(smsg.toAddress, gmsg.to[0].value); + Assert.equal(smsg.toName, gmsg.to[0].contact.name); + + // Date + Assert.equal(smsg.date.valueOf(), gmsg.date.valueOf()); + + // -- Message ID + Assert.equal(smsg.messageId, gmsg.headerMessageID); + + // -- Attachments. We won't have these if we don't have fulltext results. + if (expectFulltextResults) { + Assert.equal(gmsg.attachmentTypes.length, 1); + Assert.equal(gmsg.attachmentTypes[0], "text/plain"); + Assert.equal(gmsg.attachmentNames.length, 1); + Assert.equal(gmsg.attachmentNames[0], "bob.txt"); + + let expectedInfos = [ + // The name for that one is generated randomly. + { contentType: "message/rfc822" }, + { name: "bob.txt", contentType: "text/plain" }, + ]; + let expectedSize = 14; + Assert.equal(gmsg.attachmentInfos.length, 2); + for (let [i, attInfos] of gmsg.attachmentInfos.entries()) { + for (let k in expectedInfos[i]) { + Assert.equal(attInfos[k], expectedInfos[i][k]); + } + // Because it's unreliable and depends on the platform. + Assert.ok(Math.abs(attInfos.size - expectedSize) <= 2); + // Check that the attachment URLs are correct. + let channel = NetUtil.newChannel({ + uri: attInfos.url, + loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + securityFlags: + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + + try { + // Will throw if the URL is invalid. + channel.asyncOpen(new PromiseTestUtils.PromiseStreamListener()); + } catch (e) { + do_throw(new Error("Invalid attachment URL")); + } + } + } else { + // Make sure we don't actually get attachments! + Assert.equal(gmsg.attachmentTypes, null); + Assert.equal(gmsg.attachmentNames, null); + } +} + +/** + * We now move the message into another folder, wait for it to be indexed, + * and make sure the magic url getter for GlodaAttachment returns a proper + * URL. + */ +async function test_moved_message_attributes() { + if (!expectFulltextResults) { + return; + } + + // Don't ask me why, let destFolder = MessageInjection.make_empty_folder would result in a + // random error when running test_index_messages_imap_offline.js ... + let [[destFolder], ignoreSet] = await messageInjection.makeFoldersWithSets( + 1, + [{ count: 2 }] + ); + fundamentalFolderHandle = destFolder; + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([ignoreSet])); + + // This is a fast move (third parameter set to true). + await messageInjection.moveMessages(fundamentalMsgSet, destFolder, true); + + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([fundamentalMsgSet], { + verifier(newSynMsg, newGlodaMsg) { + // Verify we still have the same number of attachments. + Assert.equal( + fundamentalGlodaMsgAttachmentUrls.length, + newGlodaMsg.attachmentInfos.length + ); + for (let [i, attInfos] of newGlodaMsg.attachmentInfos.entries()) { + // Verify the url has changed. + Assert.notEqual(fundamentalGlodaMsgAttachmentUrls[i], attInfos.url); + // And verify that the new url is still valid. + let channel = NetUtil.newChannel({ + uri: attInfos.url, + loadingPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + securityFlags: + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER, + }); + try { + channel.asyncOpen(new PromiseTestUtils.PromiseStreamListener()); + } catch (e) { + new Error("Invalid attachment URL"); + } + } + }, + fullyIndexed: 0, + }) + ); +} + +/** + * We want to make sure that all of the fundamental properties also are there + * when we load them from disk. Nuke our cache, query the message back up. + * We previously used getMessagesByMessageID to get the message back, but he + * does not perform a full load-out like a query does, so we need to use our + * query mechanism for this. + */ +async function test_attributes_fundamental_from_disk() { + nukeGlodaCachesAndCollections(); + + let query = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE).id( + fundamentalGlodaMessageId + ); + await queryExpect( + query, + [fundamentalSyntheticMessage], + verify_attributes_fundamental_from_disk, + function (smsg) { + return smsg.messageId; + } + ); +} + +/** + * We are just a wrapper around verify_attributes_fundamental, adapting the + * return callback from getMessagesByMessageID. + * + * @param aGlodaMessageLists This should be [[theGlodaMessage]]. + */ +function verify_attributes_fundamental_from_disk(aGlodaMessage) { + // Teturn the message id for test_attributes_fundamental_from_disk's benefit. + verify_attributes_fundamental(fundamentalSyntheticMessage, aGlodaMessage); + return aGlodaMessage.headerMessageID; +} + +/* ===== Explicit Attributes (per GlodaExplicitAttr.jsm) ===== */ + +/** + * Test the attributes defined by GlodaExplicitAttr.jsm. + */ +async function test_attributes_explicit() { + let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + let gmsg = msgSet.glodaMessages[0]; + + // -- Star + msgSet.setStarred(true); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.starred, true); + + msgSet.setStarred(false); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.starred, false); + + // -- Read / Unread + msgSet.setRead(true); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.read, true); + + msgSet.setRead(false); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.read, false); + + // -- Tags + // Note that the tag service does not guarantee stable nsIMsgTag references, + // nor does noun_tag go too far out of its way to provide stability. + // However, it is stable as long as we don't spook it by bringing new tags + // into the equation. + let tagOne = TagNoun.getTag("$label1"); + let tagTwo = TagNoun.getTag("$label2"); + + msgSet.addTag(tagOne.key); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.notEqual(gmsg.tags.indexOf(tagOne), -1); + + msgSet.addTag(tagTwo.key); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.notEqual(gmsg.tags.indexOf(tagOne), -1); + Assert.notEqual(gmsg.tags.indexOf(tagTwo), -1); + + msgSet.removeTag(tagOne.key); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.tags.indexOf(tagOne), -1); + Assert.notEqual(gmsg.tags.indexOf(tagTwo), -1); + + msgSet.removeTag(tagTwo.key); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.tags.indexOf(tagOne), -1); + Assert.equal(gmsg.tags.indexOf(tagTwo), -1); + + // -- Replied To + + // -- Forwarded +} + +/** + * Test non-query-able attributes + */ +async function test_attributes_cant_query() { + let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + let gmsg = msgSet.glodaMessages[0]; + + // -- Star + msgSet.setStarred(true); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.starred, true); + + msgSet.setStarred(false); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.starred, false); + + // -- Read / Unread + msgSet.setRead(true); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.read, true); + + msgSet.setRead(false); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + Assert.equal(gmsg.read, false); + + let readDbAttr = Gloda.getAttrDef(GlodaConstants.BUILT_IN, "read"); + let readId = readDbAttr.id; + + await sqlExpectCount( + 0, + "SELECT COUNT(*) FROM messageAttributes WHERE attributeID = ?1", + readId + ); + + // -- Replied To + + // -- Forwarded +} + +/** + * Have the participants be in our addressbook prior to indexing so that we can + * verify that the hand-off to the addressbook indexer does not cause breakage. + */ +async function test_people_in_addressbook() { + var senderPair = msgGen.makeNameAndAddress(), + recipPair = msgGen.makeNameAndAddress(); + + // - Add both people to the address book. + makeABCardForAddressPair(senderPair); + makeABCardForAddressPair(recipPair); + + let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1, to: [recipPair], from: senderPair }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + let gmsg = msgSet.glodaMessages[0], + senderIdentity = gmsg.from, + recipIdentity = gmsg.to[0]; + + Assert.notEqual(senderIdentity.contact, null); + Assert.ok(senderIdentity.inAddressBook); + + Assert.notEqual(recipIdentity.contact, null); + Assert.ok(recipIdentity.inAddressBook); +} + +/* ===== Fulltexts Indexing ===== */ + +/** + * Make sure that we are using the saneBodySize flag. This is basically the + * test_sane_bodies test from test_mime_emitter but we pull the indexedBodyText + * off the message to check and also make sure that the text contents slice + * off the end rather than the beginning. + */ +async function test_streamed_bodies_are_size_capped() { + if (!expectFulltextResults) { + return; + } + + let hugeString = + "qqqqxxxx qqqqxxx qqqqxxx qqqqxxx qqqqxxx qqqqxxx qqqqxxx \r\n"; + const powahsOfTwo = 10; + for (let i = 0; i < powahsOfTwo; i++) { + hugeString = hugeString + hugeString; + } + let bodyString = "aabb" + hugeString + "xxyy"; + + let synMsg = msgGen.makeMessage({ + body: { body: bodyString, contentType: "text/plain" }, + }); + let msgSet = new SyntheticMessageSet([synMsg]); + let folder = await messageInjection.makeEmptyFolder(); + await messageInjection.addSetsToFolders([folder], [msgSet]); + + if (goOffline) { + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + await messageInjection.makeFolderAndContentsOffline(folder); + } + + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + let gmsg = msgSet.glodaMessages[0]; + Assert.ok(gmsg.indexedBodyText.startsWith("aabb")); + Assert.ok(!gmsg.indexedBodyText.includes("xxyy")); + + if (gmsg.indexedBodyText.length > 20 * 1024 + 58 + 10) { + do_throw( + "Indexed body text is too big! (" + gmsg.indexedBodyText.length + ")" + ); + } +} + +/* ===== Message Deletion ===== */ +/** + * Test actually deleting a message on a per-message basis (not just nuking the + * folder like emptying the trash does.) + * + * Logic situations: + * - Non-last message in a conversation, twin. + * - Non-last message in a conversation, not a twin. + * - Last message in a conversation + */ +async function test_message_deletion() { + // Non-last message in conv, twin. + // Create and index two messages in a conversation. + let [, convSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 2, msgsPerThread: 2 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([convSet], { augment: true })); + + // Twin the first message in a different folder owing to our reliance on + // message-id's in the SyntheticMessageSet logic. (This is also why we broke + // up the indexing waits too.) + let twinFolder = await messageInjection.makeEmptyFolder(); + let twinSet = new SyntheticMessageSet([convSet.synMessages[0]]); + await messageInjection.addSetsToFolders([twinFolder], [twinSet]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([twinSet], { augment: true })); + + // Split the conv set into two helper sets. + let firstSet = convSet.slice(0, 1); // The twinned first message in the thread. + let secondSet = convSet.slice(1, 2); // The un-twinned second thread message. + + // Make sure we can find the message (paranoia). + let firstQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE); + firstQuery.id(firstSet.glodaMessages[0].id); + let firstColl = await queryExpect(firstQuery, firstSet); + + // Delete it (not trash! delete!). + await MessageInjection.deleteMessages(firstSet); + // Which should result in an apparent deletion. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [firstSet] })); + // And our collection from that query should now be empty. + Assert.equal(firstColl.items.length, 0); + + // Make sure it no longer shows up in a standard query. + firstColl = await queryExpect(firstQuery, []); + + // Make sure it shows up in a privileged query. + let privQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, { + noDbQueryValidityConstraints: true, + }); + let firstGlodaId = firstSet.glodaMessages[0].id; + privQuery.id(firstGlodaId); + await queryExpect(privQuery, firstSet); + + // Force a deletion pass. + GlodaMsgIndexer.indexingSweepNeeded = true; + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); + + // Make sure it no longer shows up in a privileged query; since it has a twin + // we don't need to leave it as a ghost. + await queryExpect(privQuery, []); + + // Make sure that the messagesText entry got blown away. + await sqlExpectCount( + 0, + "SELECT COUNT(*) FROM messagesText WHERE docid = ?1", + firstGlodaId + ); + + // Make sure the conversation still exists. + let conv = twinSet.glodaMessages[0].conversation; + let convQuery = Gloda.newQuery(GlodaConstants.NOUN_CONVERSATION); + convQuery.id(conv.id); + let convColl = await queryExpect(convQuery, [conv]); + + // -- Non-last message, no longer a twin => ghost. + + // Make sure nuking the twin didn't somehow kill them both. + let twinQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE); + // Let's search on the message-id now that there is no ambiguity. + twinQuery.headerMessageID(twinSet.synMessages[0].messageId); + let twinColl = await queryExpect(twinQuery, twinSet); + + // Delete the twin. + await MessageInjection.deleteMessages(twinSet); + // Which should result in an apparent deletion. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [twinSet] })); + // It should disappear from the collection. + Assert.equal(twinColl.items.length, 0); + + // No longer show up in the standard query. + twinColl = await queryExpect(twinQuery, []); + + // Still show up in a privileged query. + privQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, { + noDbQueryValidityConstraints: true, + }); + privQuery.headerMessageID(twinSet.synMessages[0].messageId); + await queryExpect(privQuery, twinSet); + + // Force a deletion pass. + GlodaMsgIndexer.indexingSweepNeeded = true; + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); + + // The message should be marked as a ghost now that the deletion pass. + // Ghosts have no fulltext rows, so check for that. + await sqlExpectCount( + 0, + "SELECT COUNT(*) FROM messagesText WHERE docid = ?1", + twinSet.glodaMessages[0].id + ); + + // It still should show up in the privileged query; it's a ghost! + let privColl = await queryExpect(privQuery, twinSet); + // Make sure it looks like a ghost. + let twinGhost = privColl.items[0]; + Assert.equal(twinGhost._folderID, null); + Assert.equal(twinGhost._messageKey, null); + + // Make sure the conversation still exists. + await queryExpect(convQuery, [conv]); + + // -- Non-last message, not a twin. + // This should blow away the message, the ghosts, and the conversation. + + // Second message should still be around. + let secondQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE); + secondQuery.headerMessageID(secondSet.synMessages[0].messageId); + let secondColl = await queryExpect(secondQuery, secondSet); + + // Delete it and make sure it gets marked deleted appropriately. + await MessageInjection.deleteMessages(secondSet); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [secondSet] })); + Assert.equal(secondColl.items.length, 0); + + // Still show up in a privileged query. + privQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, { + noDbQueryValidityConstraints: true, + }); + privQuery.headerMessageID(secondSet.synMessages[0].messageId); + await queryExpect(privQuery, secondSet); + + // Force a deletion pass. + GlodaMsgIndexer.indexingSweepNeeded = true; + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); + + // It should no longer show up in a privileged query; we killed the ghosts. + await queryExpect(privQuery, []); + + // - The conversation should have disappeared too. + // (we have no listener to watch for it to have disappeared from convQuery but + // this is basically how glodaTestHelper does its thing anyways.) + Assert.equal(convColl.items.length, 0); + + // Make sure the query fails to find it too. + await queryExpect(convQuery, []); + + // -- Identity culling verification. + // The identities associated with that message should no longer exist, nor + // should their contacts. +} + +async function test_moving_to_trash_marks_deletion() { + // Create and index two messages in a conversation. + let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 2, msgsPerThread: 2 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + + let convId = msgSet.glodaMessages[0].conversation.id; + let firstGlodaId = msgSet.glodaMessages[0].id; + let secondGlodaId = msgSet.glodaMessages[1].id; + + // Move them to the trash. + await messageInjection.trashMessages(msgSet); + + // We do not index the trash folder so this should actually make them appear + // deleted to an unprivileged query. + let msgQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE); + msgQuery.id(firstGlodaId, secondGlodaId); + await queryExpect(msgQuery, []); + + // They will appear deleted after the events. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] })); + + // Force a sweep. + GlodaMsgIndexer.indexingSweepNeeded = true; + // There should be no apparent change as the result of this pass. + // Well, the conversation will die, but we can't see that. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); + + // The conversation should be gone. + let convQuery = Gloda.newQuery(GlodaConstants.NOUN_CONVERSATION); + convQuery.id(convId); + await queryExpect(convQuery, []); + + // The messages should be entirely gone. + let msgPrivQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, { + noDbQueryValidityConstraints: true, + }); + msgPrivQuery.id(firstGlodaId, secondGlodaId); + await queryExpect(msgPrivQuery, []); +} + +/** + * Deletion that occurs because a folder got deleted. + * There is no hand-holding involving the headers that were in the folder. + */ +async function test_folder_nuking_message_deletion() { + // Create and index two messages in a conversation. + let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 2, msgsPerThread: 2 }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { augment: true })); + + let convId = msgSet.glodaMessages[0].conversation.id; + let firstGlodaId = msgSet.glodaMessages[0].id; + let secondGlodaId = msgSet.glodaMessages[1].id; + + // Delete the folder. + messageInjection.deleteFolder(folder); + // That does generate the deletion events if the messages were in-memory, + // which these are. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([], { deleted: [msgSet] })); + + // This should have caused us to mark all the messages as deleted; the + // messages should no longer show up in an unprivileged query. + let msgQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE); + msgQuery.id(firstGlodaId, secondGlodaId); + await queryExpect(msgQuery, []); + + // Force a sweep. + GlodaMsgIndexer.indexingSweepNeeded = true; + // There should be no apparent change as the result of this pass. + // Well, the conversation will die, but we can't see that. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); + + // The conversation should be gone. + let convQuery = Gloda.newQuery(GlodaConstants.NOUN_CONVERSATION); + convQuery.id(convId); + await queryExpect(convQuery, []); + + // The messages should be entirely gone. + let msgPrivQuery = Gloda.newQuery(GlodaConstants.NOUN_MESSAGE, { + noDbQueryValidityConstraints: true, + }); + msgPrivQuery.id(firstGlodaId, secondGlodaId); + await queryExpect(msgPrivQuery, []); +} + +/* ===== Folder Move/Rename/Copy (Single and Nested) ===== */ + +async function test_folder_deletion_nested() { + // Add a folder with a bunch of messages. + let [[folder1], msgSet1] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + + let [[folder2], msgSet2] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + + // Index these folders, and augment the msgSet with the glodaMessages array + // for later use by sqlExpectCount. + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([msgSet1, msgSet2], { augment: true }) + ); + // The move has to be performed after the indexing, because otherwise, on + // IMAP, the moved message header are different entities and it's not msgSet2 + // that ends up indexed, but the fresh headers + await MessageInjection.moveFolder(folder2, folder1); + + // Add a trash folder, and move folder1 into it. + let trash = await messageInjection.makeEmptyFolder(null, [ + Ci.nsMsgFolderFlags.Trash, + ]); + await MessageInjection.moveFolder(folder1, trash); + + let folders = MessageInjection.get_nsIMsgFolder(trash).descendants; + Assert.equal(folders.length, 2); + let [newFolder1, newFolder2] = folders; + + let glodaFolder1 = Gloda.getFolderForFolder(newFolder1); + let glodaFolder2 = Gloda.getFolderForFolder(newFolder2); + + // Verify that Gloda properly marked this folder as not to be indexed anymore. + Assert.equal( + glodaFolder1.indexingPriority, + glodaFolder1.kIndexingNeverPriority + ); + + // Check that existing message is marked as deleted. + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([], { deleted: [msgSet1, msgSet2] }) + ); + + // Make sure the deletion hit the database. + await sqlExpectCount( + 1, + "SELECT COUNT(*) from folderLocations WHERE id = ? AND indexingPriority = ?", + glodaFolder1.id, + glodaFolder1.kIndexingNeverPriority + ); + await sqlExpectCount( + 1, + "SELECT COUNT(*) from folderLocations WHERE id = ? AND indexingPriority = ?", + glodaFolder2.id, + glodaFolder2.kIndexingNeverPriority + ); + + if (messageInjection.messageInjectionIsLocal()) { + // Add another message. + await messageInjection.makeNewSetsInFolders([newFolder1], [{ count: 1 }]); + await messageInjection.makeNewSetsInFolders([newFolder2], [{ count: 1 }]); + + // Make sure that indexing returns nothing. + GlodaMsgIndexer.indexingSweepNeeded = true; + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); + } +} + +/* ===== IMAP Nuances ===== */ + +/** + * Verify that for IMAP folders we still see an index a message that is added + * as read. + */ +async function test_imap_add_unread_to_folder() { + if (messageInjection.messageInjectionIsLocal()) { + return; + } + + let [, msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1, read: true }, + ]); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); +} + +/* ===== Message Moving ===== */ + +/** + * Moving a message between folders should result in us knowing that the message + * is in the target location. + */ +async function test_message_moving() { + // - Inject and insert. + // Source folder with the message we care about. + let [[srcFolder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + // Dest folder with some messages in it to test some wacky local folder moving + // logic. (Local moves try and update the correspondence immediately.) + let [[destFolder], ignoreSet] = await messageInjection.makeFoldersWithSets( + 1, + [{ count: 2 }] + ); + + // We want the gloda message mapping. + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([msgSet, ignoreSet], { augment: true }) + ); + let gmsg = msgSet.glodaMessages[0]; + // Save off the message key so we can make sure it changes. + let oldMessageKey = msgSet.getMsgHdr(0).messageKey; + + // - Fastpath (offline) move it to a new folder. + // Initial move. + await messageInjection.moveMessages(msgSet, destFolder, true); + + // - Make sure gloda sees it in the new folder. + // Since we are doing offline IMAP moves, the fast-path should be taken and + // so we should receive an itemsModified notification without a call to + // Gloda.grokNounItem. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet], { fullyIndexed: 0 })); + + Assert.equal( + gmsg.folderURI, + messageInjection.getRealInjectionFolder(destFolder).URI + ); + + // - Make sure the message key is correct! + Assert.equal(gmsg.messageKey, msgSet.getMsgHdr(0).messageKey); + // Sanity check that the messageKey actually changed for the message. + Assert.notEqual(gmsg.messageKey, oldMessageKey); + + // - Make sure the indexer's _keyChangedBatchInfo dict is empty. + for (let evilKey in GlodaMsgIndexer._keyChangedBatchInfo) { + let evilValue = GlodaMsgIndexer._keyChangedBatchInfo[evilKey]; + throw new Error( + "GlodaMsgIndexer._keyChangedBatchInfo should be empty but" + + "has key:\n" + + evilKey + + "\nAnd value:\n", + evilValue + "." + ); + } + + // - Slowpath (IMAP online) move it back to its origin folder. + // Move it back. + await messageInjection.moveMessages(msgSet, srcFolder, false); + // In the IMAP case we will end up reindexing the message because we will + // not be able to fast-path, but the local case will still be fast-pathed. + await waitForGlodaIndexer(); + Assert.ok( + ...assertExpectedMessagesIndexed([msgSet], { + fullyIndexed: messageInjection.messageInjectionIsLocal() ? 0 : 1, + }) + ); + Assert.equal( + gmsg.folderURI, + messageInjection.getRealInjectionFolder(srcFolder).URI + ); + Assert.equal(gmsg.messageKey, msgSet.getMsgHdr(0).messageKey); +} + +/** + * Moving a gloda-indexed message out of a filthy folder should result in the + * destination message not having a gloda-id. + */ + +/* ===== Message Copying ===== */ + +/* ===== Sweep Complications ==== */ + +/** + * Make sure that a message indexed by event-driven indexing does not + * get reindexed by sweep indexing that follows. + */ +async function test_sweep_indexing_does_not_reindex_event_indexed() { + let [[folder], msgSet] = await messageInjection.makeFoldersWithSets(1, [ + { count: 1 }, + ]); + + // Wait for the event sweep to complete. + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); + + // Force a sweep of the folder. + GlodaMsgIndexer.indexFolder(messageInjection.getRealInjectionFolder(folder)); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([])); +} + +/** + * Verify that moving apparently gloda-indexed messages from a filthy folder or + * one that simply should not be gloda indexed does not result in the target + * messages having the gloda-id property on them. To avoid messing with too + * many invariants we do the 'folder should not be gloda indexed' case. + * Uh, and of course, the message should still get indexed once we clear the + * filthy gloda-id off of it given that it is moving from a folder that is not + * indexed to one that is indexed. + */ +async function test_filthy_moves_slash_move_from_unindexed_to_indexed() { + // - Inject. + // The source folder needs a flag so we don't index it. + let srcFolder = await messageInjection.makeEmptyFolder(null, [ + Ci.nsMsgFolderFlags.Junk, + ]); + // The destination folder has to be something we want to index though. + let destFolder = await messageInjection.makeEmptyFolder(); + let [msgSet] = await messageInjection.makeNewSetsInFolders( + [srcFolder], + [{ count: 1 }] + ); + + // - Mark with a bogus gloda-id. + msgSet.getMsgHdr(0).setUint32Property("gloda-id", 9999); + + // - Disable event driven indexing so we don't get interference from indexing. + configureGlodaIndexing({ event: false }); + + // - Move. + await messageInjection.moveMessages(msgSet, destFolder); + + // - Verify the target has no gloda-id! + dump(`checking ${msgSet.getMsgHdr(0)}`); + Assert.equal(msgSet.getMsgHdr(0).getUint32Property("gloda-id"), 0); + + // - Re-enable indexing and let the indexer run. + // We don't want to affect other tests. + configureGlodaIndexing({}); + await waitForGlodaIndexer(); + Assert.ok(...assertExpectedMessagesIndexed([msgSet])); +} + +function test_sanity_test_environment() { + Assert.ok(msgGen, "Sanity that msgGen is set."); + Assert.ok(scenarios, "Sanity that scenarios is set"); + Assert.ok(messageInjection, "Sanity that messageInjection is set."); +} + +var base_index_messages_tests = [ + test_sanity_test_environment, + test_pending_commit_tracker_flushes_correctly, + test_pending_commit_causes_msgdb_commit, + test_indexing_sweep, + test_event_driven_indexing_does_not_mess_with_filthy_folders, + + test_threading_direct_reply, + test_threading_missing_intermediary, + test_threading_siblings_missing_parent, + test_attachment_flag, + test_attributes_fundamental, + test_moved_message_attributes, + test_attributes_fundamental_from_disk, + test_attributes_explicit, + test_attributes_cant_query, + + test_people_in_addressbook, + + test_streamed_bodies_are_size_capped, + + test_imap_add_unread_to_folder, + test_message_moving, + + test_message_deletion, + test_moving_to_trash_marks_deletion, + test_folder_nuking_message_deletion, + + test_sweep_indexing_does_not_reindex_event_indexed, + + test_filthy_moves_slash_move_from_unindexed_to_indexed, + + test_indexing_never_priority, + test_setting_indexing_priority_never_while_indexing, + + test_folder_deletion_nested, +]; |