diff options
Diffstat (limited to 'comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js')
-rw-r--r-- | comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js | 2043 |
1 files changed, 2043 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js new file mode 100644 index 0000000000..8fcc3ca14f --- /dev/null +++ b/comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js @@ -0,0 +1,2043 @@ +/* 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/. */ + +"use strict"; + +var { ExtensionTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs" +); + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + AddrBookCard: "resource:///modules/AddrBookCard.jsm", + AddrBookUtils: "resource:///modules/AddrBookUtils.jsm", +}); + +var { AddonTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/AddonTestUtils.sys.mjs" +); + +ExtensionTestUtils.mockAppInfo(); +AddonTestUtils.maybeInit(this); + +add_setup(async () => { + Services.prefs.setIntPref("ldap_2.servers.osx.dirType", -1); + + registerCleanupFunction(() => { + // Make sure any open database is given a chance to close. + Services.startup.advanceShutdownPhase( + Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED + ); + }); +}); + +add_task(async function test_addressBooks() { + async function background() { + let firstBookId, secondBookId, newContactId; + + let events = []; + let eventPromise; + let eventPromiseResolve; + for (let eventNamespace of ["addressBooks", "contacts", "mailingLists"]) { + for (let eventName of [ + "onCreated", + "onUpdated", + "onDeleted", + "onMemberAdded", + "onMemberRemoved", + ]) { + if (eventName in browser[eventNamespace]) { + browser[eventNamespace][eventName].addListener((...args) => { + events.push({ namespace: eventNamespace, name: eventName, args }); + if (eventPromiseResolve) { + let resolve = eventPromiseResolve; + eventPromiseResolve = null; + resolve(); + } + }); + } + } + } + + let outsideEvent = function (action, ...args) { + eventPromise = new Promise(resolve => { + eventPromiseResolve = resolve; + }); + return window.sendMessage("outsideEventsTest", action, ...args); + }; + let checkEvents = async function (...expectedEvents) { + if (eventPromiseResolve) { + await eventPromise; + } + + browser.test.assertEq( + expectedEvents.length, + events.length, + "Correct number of events" + ); + + if (expectedEvents.length != events.length) { + for (let event of events) { + let args = event.args.join(", "); + browser.test.log(`${event.namespace}.${event.name}(${args})`); + } + throw new Error("Wrong number of events, stopping."); + } + + for (let [namespace, name, ...expectedArgs] of expectedEvents) { + let event = events.shift(); + browser.test.assertEq( + namespace, + event.namespace, + "Event namespace is correct" + ); + browser.test.assertEq(name, event.name, "Event type is correct"); + browser.test.assertEq( + expectedArgs.length, + event.args.length, + "Argument count is correct" + ); + window.assertDeepEqual(expectedArgs, event.args); + if (expectedEvents.length == 1) { + return event.args; + } + } + + return null; + }; + + async function addressBookTest() { + browser.test.log("Starting addressBookTest"); + let list = await browser.addressBooks.list(); + browser.test.assertEq(2, list.length); + for (let b of list) { + browser.test.assertEq(5, Object.keys(b).length); + browser.test.assertEq(36, b.id.length); + browser.test.assertEq("addressBook", b.type); + browser.test.assertTrue("name" in b); + browser.test.assertFalse(b.readOnly); + browser.test.assertFalse(b.remote); + } + + let completeList = await browser.addressBooks.list(true); + browser.test.assertEq(2, completeList.length); + for (let b of completeList) { + browser.test.assertEq(7, Object.keys(b).length); + } + + firstBookId = list[0].id; + secondBookId = list[1].id; + + let firstBook = await browser.addressBooks.get(firstBookId); + browser.test.assertEq(5, Object.keys(firstBook).length); + + let secondBook = await browser.addressBooks.get(secondBookId, true); + browser.test.assertEq(7, Object.keys(secondBook).length); + browser.test.assertTrue(Array.isArray(secondBook.contacts)); + browser.test.assertEq(0, secondBook.contacts.length); + browser.test.assertTrue(Array.isArray(secondBook.mailingLists)); + browser.test.assertEq(0, secondBook.mailingLists.length); + let newBookId = await browser.addressBooks.create({ name: "test name" }); + browser.test.assertEq(36, newBookId.length); + await checkEvents([ + "addressBooks", + "onCreated", + { type: "addressBook", id: newBookId }, + ]); + + list = await browser.addressBooks.list(); + browser.test.assertEq(3, list.length); + + let newBook = await browser.addressBooks.get(newBookId); + browser.test.assertEq(newBookId, newBook.id); + browser.test.assertEq("addressBook", newBook.type); + browser.test.assertEq("test name", newBook.name); + + await browser.addressBooks.update(newBookId, { name: "new name" }); + await checkEvents([ + "addressBooks", + "onUpdated", + { type: "addressBook", id: newBookId }, + ]); + let updatedBook = await browser.addressBooks.get(newBookId); + browser.test.assertEq("new name", updatedBook.name); + + list = await browser.addressBooks.list(); + browser.test.assertEq(3, list.length); + + await browser.addressBooks.delete(newBookId); + await checkEvents(["addressBooks", "onDeleted", newBookId]); + + list = await browser.addressBooks.list(); + browser.test.assertEq(2, list.length); + + for (let operation of ["get", "update", "delete"]) { + let args = [newBookId]; + if (operation == "update") { + args.push({ name: "" }); + } + + try { + await browser.addressBooks[operation].apply( + browser.addressBooks, + args + ); + browser.test.fail( + `Calling ${operation} on a non-existent address book should throw` + ); + } catch (ex) { + browser.test.assertEq( + `addressBook with id=${newBookId} could not be found.`, + ex.message, + `browser.addressBooks.${operation} threw exception` + ); + } + } + + // Test the prevention of creating new address book with an empty name + await browser.test.assertRejects( + browser.addressBooks.create({ name: "" }), + "An unexpected error occurred", + "browser.addressBooks.create threw exception" + ); + + browser.test.assertEq(0, events.length, "No events left unconsumed"); + browser.test.log("Completed addressBookTest"); + } + + async function contactsTest() { + browser.test.log("Starting contactsTest"); + let contacts = await browser.contacts.list(firstBookId); + browser.test.assertTrue(Array.isArray(contacts)); + browser.test.assertEq(0, contacts.length); + + newContactId = await browser.contacts.create(firstBookId, { + FirstName: "first", + LastName: "last", + Notes: "Notes", + SomethingCustom: "Custom property", + }); + browser.test.assertEq(36, newContactId.length); + await checkEvents([ + "contacts", + "onCreated", + { type: "contact", parentId: firstBookId, id: newContactId }, + ]); + + contacts = await browser.contacts.list(firstBookId); + browser.test.assertEq(1, contacts.length, "Contact added to first book."); + browser.test.assertEq(contacts[0].id, newContactId); + + contacts = await browser.contacts.list(secondBookId); + browser.test.assertEq( + 0, + contacts.length, + "Contact not added to second book." + ); + + let newContact = await browser.contacts.get(newContactId); + browser.test.assertEq(6, Object.keys(newContact).length); + browser.test.assertEq(newContactId, newContact.id); + browser.test.assertEq(firstBookId, newContact.parentId); + browser.test.assertEq("contact", newContact.type); + browser.test.assertEq(false, newContact.readOnly); + browser.test.assertEq(false, newContact.remote); + browser.test.assertEq(5, Object.keys(newContact.properties).length); + browser.test.assertEq("first", newContact.properties.FirstName); + browser.test.assertEq("last", newContact.properties.LastName); + browser.test.assertEq("Notes", newContact.properties.Notes); + browser.test.assertEq( + "Custom property", + newContact.properties.SomethingCustom + ); + browser.test.assertEq( + `BEGIN:VCARD\r\nVERSION:4.0\r\nNOTE:Notes\r\nN:last;first;;;\r\nUID:${newContactId}\r\nEND:VCARD\r\n`, + newContact.properties.vCard + ); + + // Changing the UID should throw. + try { + await browser.contacts.update(newContactId, { + vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:SomethingNew\r\nEND:VCARD\r\n`, + }); + browser.test.fail( + `Updating a contact with a vCard with a differnt UID should throw` + ); + } catch (ex) { + browser.test.assertEq( + `The card's UID ${newContactId} may not be changed: BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:SomethingNew\r\nEND:VCARD\r\n.`, + ex.message, + `browser.contacts.update threw exception` + ); + } + + // Test Custom1. + { + await browser.contacts.update(newContactId, { + vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nNOTE:Notes\r\nN:last;first;;;\r\nX-CUSTOM1;VALUE=TEXT:Original custom value\r\nEND:VCARD`, + }); + await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: firstBookId, id: newContactId }, + { + Custom1: { oldValue: null, newValue: "Original custom value" }, + }, + ]); + let updContact1 = await browser.contacts.get(newContactId); + browser.test.assertEq( + "Original custom value", + updContact1.properties.Custom1 + ); + + await browser.contacts.update(newContactId, { + Custom1: "Updated custom value", + }); + await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: firstBookId, id: newContactId }, + { + Custom1: { + oldValue: "Original custom value", + newValue: "Updated custom value", + }, + }, + ]); + let updContact2 = await browser.contacts.get(newContactId); + browser.test.assertEq( + "Updated custom value", + updContact2.properties.Custom1 + ); + browser.test.assertTrue( + updContact2.properties.vCard.includes( + "X-CUSTOM1;VALUE=TEXT:Updated custom value" + ), + "vCard should include the correct x-custom1 entry" + ); + } + + // If a vCard and legacy properties are given, vCard must win. + await browser.contacts.update(newContactId, { + vCard: `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:${newContactId}\r\nEND:VCARD\r\n`, + FirstName: "Superman", + PrimaryEmail: "c.kent@dailyplanet.com", + PreferDisplayName: "0", + OtherCustom: "Yet another custom property", + Notes: "Ignored Notes", + }); + await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: firstBookId, id: newContactId }, + { + PrimaryEmail: { oldValue: null, newValue: "first@last" }, + LastName: { oldValue: "last", newValue: null }, + OtherCustom: { + oldValue: null, + newValue: "Yet another custom property", + }, + PreferDisplayName: { oldValue: null, newValue: "0" }, + Custom1: { oldValue: "Updated custom value", newValue: null }, + }, + ]); + + let updatedContact = await browser.contacts.get(newContactId); + browser.test.assertEq(6, Object.keys(updatedContact.properties).length); + browser.test.assertEq("first", updatedContact.properties.FirstName); + browser.test.assertEq( + "first@last", + updatedContact.properties.PrimaryEmail + ); + browser.test.assertTrue(!("LastName" in updatedContact.properties)); + browser.test.assertTrue( + !("Notes" in updatedContact.properties), + "The vCard is not specifying Notes and the specified Notes property should be ignored." + ); + browser.test.assertEq( + "Custom property", + updatedContact.properties.SomethingCustom, + "Untouched custom properties should not be changed by updating the vCard" + ); + browser.test.assertEq( + "Yet another custom property", + updatedContact.properties.OtherCustom, + "Custom properties should be added even while updating a vCard" + ); + browser.test.assertEq( + "0", + updatedContact.properties.PreferDisplayName, + "Setting non-banished properties parallel to a vCard should update" + ); + browser.test.assertEq( + `BEGIN:VCARD\r\nVERSION:4.0\r\nN:;first;;;\r\nEMAIL;PREF=1:first@last\r\nUID:${newContactId}\r\nEND:VCARD\r\n`, + updatedContact.properties.vCard + ); + + // Manually Remove properties. + await browser.contacts.update(newContactId, { + LastName: "lastname", + PrimaryEmail: null, + SecondEmail: "test@invalid.de", + SomethingCustom: null, + OtherCustom: null, + }); + await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: firstBookId, id: newContactId }, + { + LastName: { oldValue: null, newValue: "lastname" }, + // It is how it is. Defining a 2nd email with no 1st, will make it the first. + PrimaryEmail: { oldValue: "first@last", newValue: "test@invalid.de" }, + SomethingCustom: { oldValue: "Custom property", newValue: null }, + OtherCustom: { + oldValue: "Yet another custom property", + newValue: null, + }, + }, + ]); + + updatedContact = await browser.contacts.get(newContactId); + browser.test.assertEq(5, Object.keys(updatedContact.properties).length); + // LastName and FirstName are stored in the same multi field property and changing LastName should not change FirstName. + browser.test.assertEq("first", updatedContact.properties.FirstName); + browser.test.assertEq("lastname", updatedContact.properties.LastName); + browser.test.assertEq( + "test@invalid.de", + updatedContact.properties.PrimaryEmail + ); + browser.test.assertTrue( + !("SomethingCustom" in updatedContact.properties) + ); + browser.test.assertTrue(!("OtherCustom" in updatedContact.properties)); + browser.test.assertEq( + `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;first;;;\r\nEMAIL:test@invalid.de\r\nUID:${newContactId}\r\nEND:VCARD\r\n`, + updatedContact.properties.vCard + ); + + // Add an email address, going from 1 to 2.Also remove FirstName, LastName should stay. + await browser.contacts.update(newContactId, { + FirstName: null, + PrimaryEmail: "new1@invalid.de", + SecondEmail: "new2@invalid.de", + }); + await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: firstBookId, id: newContactId }, + { + PrimaryEmail: { + oldValue: "test@invalid.de", + newValue: "new1@invalid.de", + }, + SecondEmail: { oldValue: null, newValue: "new2@invalid.de" }, + FirstName: { oldValue: "first", newValue: null }, + }, + ]); + + updatedContact = await browser.contacts.get(newContactId); + browser.test.assertEq(5, Object.keys(updatedContact.properties).length); + browser.test.assertEq("lastname", updatedContact.properties.LastName); + browser.test.assertEq( + "new1@invalid.de", + updatedContact.properties.PrimaryEmail + ); + browser.test.assertEq( + "new2@invalid.de", + updatedContact.properties.SecondEmail + ); + browser.test.assertEq( + `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;;;;\r\nEMAIL;PREF=1:new1@invalid.de\r\nUID:${newContactId}\r\nEMAIL:new2@invalid.de\r\nEND:VCARD\r\n`, + updatedContact.properties.vCard + ); + + // Remove and email address, going from 2 to 1. + await browser.contacts.update(newContactId, { + SecondEmail: null, + }); + await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: firstBookId, id: newContactId }, + { + SecondEmail: { oldValue: "new2@invalid.de", newValue: null }, + }, + ]); + + updatedContact = await browser.contacts.get(newContactId); + browser.test.assertEq(4, Object.keys(updatedContact.properties).length); + browser.test.assertEq("lastname", updatedContact.properties.LastName); + browser.test.assertEq( + "new1@invalid.de", + updatedContact.properties.PrimaryEmail + ); + browser.test.assertEq( + `BEGIN:VCARD\r\nVERSION:4.0\r\nN:lastname;;;;\r\nEMAIL;PREF=1:new1@invalid.de\r\nUID:${newContactId}\r\nEND:VCARD\r\n`, + updatedContact.properties.vCard + ); + + // Set a fixed UID. + let fixedContactId = await browser.contacts.create( + firstBookId, + "this is a test", + { + FirstName: "a", + LastName: "test", + } + ); + browser.test.assertEq("this is a test", fixedContactId); + await checkEvents([ + "contacts", + "onCreated", + { type: "contact", parentId: firstBookId, id: "this is a test" }, + ]); + + let fixedContact = await browser.contacts.get("this is a test"); + browser.test.assertEq("this is a test", fixedContact.id); + + await browser.contacts.delete("this is a test"); + await checkEvents([ + "contacts", + "onDeleted", + firstBookId, + "this is a test", + ]); + + try { + await browser.contacts.create(firstBookId, newContactId, { + FirstName: "uh", + LastName: "oh", + }); + browser.test.fail(`Adding a contact with a duplicate id should throw`); + } catch (ex) { + browser.test.assertEq( + `Duplicate contact id: ${newContactId}`, + ex.message, + `browser.contacts.create threw exception` + ); + } + + browser.test.assertEq(0, events.length, "No events left unconsumed"); + browser.test.log("Completed contactsTest"); + } + + async function mailingListsTest() { + browser.test.log("Starting mailingListsTest"); + let mailingLists = await browser.mailingLists.list(firstBookId); + browser.test.assertTrue(Array.isArray(mailingLists)); + browser.test.assertEq(0, mailingLists.length); + + let newMailingListId = await browser.mailingLists.create(firstBookId, { + name: "name", + }); + browser.test.assertEq(36, newMailingListId.length); + await checkEvents([ + "mailingLists", + "onCreated", + { type: "mailingList", parentId: firstBookId, id: newMailingListId }, + ]); + + mailingLists = await browser.mailingLists.list(firstBookId); + browser.test.assertEq( + 1, + mailingLists.length, + "List added to first book." + ); + + mailingLists = await browser.mailingLists.list(secondBookId); + browser.test.assertEq( + 0, + mailingLists.length, + "List not added to second book." + ); + + let newAddressList = await browser.mailingLists.get(newMailingListId); + browser.test.assertEq(8, Object.keys(newAddressList).length); + browser.test.assertEq(newMailingListId, newAddressList.id); + browser.test.assertEq(firstBookId, newAddressList.parentId); + browser.test.assertEq("mailingList", newAddressList.type); + browser.test.assertEq("name", newAddressList.name); + browser.test.assertEq("", newAddressList.nickName); + browser.test.assertEq("", newAddressList.description); + browser.test.assertEq(false, newAddressList.readOnly); + browser.test.assertEq(false, newAddressList.remote); + + // Test that a valid name is ensured for an existing mail list + await browser.test.assertRejects( + browser.mailingLists.update(newMailingListId, { + name: "", + }), + "An unexpected error occurred", + "browser.mailingLists.update threw exception" + ); + + await browser.test.assertRejects( + browser.mailingLists.update(newMailingListId, { + name: "Two spaces invalid name", + }), + "An unexpected error occurred", + "browser.mailingLists.update threw exception" + ); + + await browser.test.assertRejects( + browser.mailingLists.update(newMailingListId, { + name: "><<<", + }), + "An unexpected error occurred", + "browser.mailingLists.update threw exception" + ); + + await browser.mailingLists.update(newMailingListId, { + name: "name!", + nickName: "nickname!", + description: "description!", + }); + await checkEvents([ + "mailingLists", + "onUpdated", + { type: "mailingList", parentId: firstBookId, id: newMailingListId }, + ]); + + let updatedMailingList = await browser.mailingLists.get(newMailingListId); + browser.test.assertEq("name!", updatedMailingList.name); + browser.test.assertEq("nickname!", updatedMailingList.nickName); + browser.test.assertEq("description!", updatedMailingList.description); + + await browser.mailingLists.addMember(newMailingListId, newContactId); + await checkEvents([ + "mailingLists", + "onMemberAdded", + { type: "contact", parentId: newMailingListId, id: newContactId }, + ]); + + let listMembers = await browser.mailingLists.listMembers( + newMailingListId + ); + browser.test.assertTrue(Array.isArray(listMembers)); + browser.test.assertEq(1, listMembers.length); + + let anotherContactId = await browser.contacts.create(firstBookId, { + FirstName: "second", + LastName: "last", + PrimaryEmail: "em@il", + }); + await checkEvents([ + "contacts", + "onCreated", + { + type: "contact", + parentId: firstBookId, + id: anotherContactId, + readOnly: false, + }, + ]); + + await browser.mailingLists.addMember(newMailingListId, anotherContactId); + await checkEvents([ + "mailingLists", + "onMemberAdded", + { type: "contact", parentId: newMailingListId, id: anotherContactId }, + ]); + + listMembers = await browser.mailingLists.listMembers(newMailingListId); + browser.test.assertEq(2, listMembers.length); + + await browser.contacts.delete(anotherContactId); + await checkEvents( + ["contacts", "onDeleted", firstBookId, anotherContactId], + ["mailingLists", "onMemberRemoved", newMailingListId, anotherContactId] + ); + listMembers = await browser.mailingLists.listMembers(newMailingListId); + browser.test.assertEq(1, listMembers.length); + + await browser.mailingLists.removeMember(newMailingListId, newContactId); + await checkEvents([ + "mailingLists", + "onMemberRemoved", + newMailingListId, + newContactId, + ]); + listMembers = await browser.mailingLists.listMembers(newMailingListId); + browser.test.assertEq(0, listMembers.length); + + await browser.mailingLists.delete(newMailingListId); + await checkEvents([ + "mailingLists", + "onDeleted", + firstBookId, + newMailingListId, + ]); + + mailingLists = await browser.mailingLists.list(firstBookId); + browser.test.assertEq(0, mailingLists.length); + + for (let operation of [ + "get", + "update", + "delete", + "listMembers", + "addMember", + "removeMember", + ]) { + let args = [newMailingListId]; + switch (operation) { + case "update": + args.push({ name: "" }); + break; + case "addMember": + case "removeMember": + args.push(newContactId); + break; + } + + try { + await browser.mailingLists[operation].apply( + browser.mailingLists, + args + ); + browser.test.fail( + `Calling ${operation} on a non-existent mailing list should throw` + ); + } catch (ex) { + browser.test.assertEq( + `mailingList with id=${newMailingListId} could not be found.`, + ex.message, + `browser.mailingLists.${operation} threw exception` + ); + } + } + + // Test that a valid name is ensured for a new mail list + await browser.test.assertRejects( + browser.mailingLists.create(firstBookId, { + name: "", + }), + "An unexpected error occurred", + "browser.mailingLists.update threw exception" + ); + + await browser.test.assertRejects( + browser.mailingLists.create(firstBookId, { + name: "Two spaces invalid name", + }), + "An unexpected error occurred", + "browser.mailingLists.update threw exception" + ); + + await browser.test.assertRejects( + browser.mailingLists.create(firstBookId, { + name: "><<<", + }), + "An unexpected error occurred", + "browser.mailingLists.update threw exception" + ); + + browser.test.assertEq(0, events.length, "No events left unconsumed"); + browser.test.log("Completed mailingListsTest"); + } + + async function contactRemovalTest() { + browser.test.log("Starting contactRemovalTest"); + await browser.contacts.delete(newContactId); + await checkEvents(["contacts", "onDeleted", firstBookId, newContactId]); + + for (let operation of ["get", "update", "delete"]) { + let args = [newContactId]; + if (operation == "update") { + args.push({}); + } + + try { + await browser.contacts[operation].apply(browser.contacts, args); + browser.test.fail( + `Calling ${operation} on a non-existent contact should throw` + ); + } catch (ex) { + browser.test.assertEq( + `contact with id=${newContactId} could not be found.`, + ex.message, + `browser.contacts.${operation} threw exception` + ); + } + } + + let contacts = await browser.contacts.list(firstBookId); + browser.test.assertEq(0, contacts.length); + + browser.test.assertEq(0, events.length, "No events left unconsumed"); + browser.test.log("Completed contactRemovalTest"); + } + + async function outsideEventsTest() { + browser.test.log("Starting outsideEventsTest"); + let [bookId, newBookPrefId] = await outsideEvent("createAddressBook"); + let [newBook] = await checkEvents([ + "addressBooks", + "onCreated", + { type: "addressBook", id: bookId }, + ]); + browser.test.assertEq("external add", newBook.name); + + await outsideEvent("updateAddressBook", newBookPrefId); + let [updatedBook] = await checkEvents([ + "addressBooks", + "onUpdated", + { type: "addressBook", id: bookId }, + ]); + browser.test.assertEq("external edit", updatedBook.name); + + await outsideEvent("deleteAddressBook", newBookPrefId); + await checkEvents(["addressBooks", "onDeleted", bookId]); + + let [parentId1, contactId] = await outsideEvent("createContact"); + let [newContact] = await checkEvents([ + "contacts", + "onCreated", + { type: "contact", parentId: parentId1, id: contactId }, + ]); + browser.test.assertEq("external", newContact.properties.FirstName); + browser.test.assertEq("add", newContact.properties.LastName); + browser.test.assertTrue( + newContact.properties.vCard.includes("VERSION:4.0"), + "vCard should be version 4.0" + ); + + // Update the contact from outside. + await outsideEvent("updateContact", contactId); + let [updatedContact] = await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: parentId1, id: contactId }, + { LastName: { oldValue: "add", newValue: "edit" } }, + ]); + browser.test.assertEq("external", updatedContact.properties.FirstName); + browser.test.assertEq("edit", updatedContact.properties.LastName); + + let [parentId2, listId] = await outsideEvent("createMailingList"); + let [newList] = await checkEvents([ + "mailingLists", + "onCreated", + { type: "mailingList", parentId: parentId2, id: listId }, + ]); + browser.test.assertEq("external add", newList.name); + + await outsideEvent("updateMailingList", listId); + let [updatedList] = await checkEvents([ + "mailingLists", + "onUpdated", + { type: "mailingList", parentId: parentId2, id: listId }, + ]); + browser.test.assertEq("external edit", updatedList.name); + + await outsideEvent("addMailingListMember", listId, contactId); + await checkEvents([ + "mailingLists", + "onMemberAdded", + { type: "contact", parentId: listId, id: contactId }, + ]); + let listMembers = await browser.mailingLists.listMembers(listId); + browser.test.assertEq(1, listMembers.length); + + await outsideEvent("removeMailingListMember", listId, contactId); + await checkEvents(["mailingLists", "onMemberRemoved", listId, contactId]); + + await outsideEvent("deleteMailingList", listId); + await checkEvents(["mailingLists", "onDeleted", parentId2, listId]); + + await outsideEvent("deleteContact", contactId); + await checkEvents(["contacts", "onDeleted", parentId1, contactId]); + + browser.test.log("Completed outsideEventsTest"); + } + + await addressBookTest(); + await contactsTest(); + await mailingListsTest(); + await contactRemovalTest(); + await outsideEventsTest(); + + browser.test.notifyPass("addressBooks"); + } + + let extension = ExtensionTestUtils.loadExtension({ + files: { + "background.js": background, + "utils.js": await getUtilsJS(), + }, + manifest: { + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["addressBooks"], + }, + }); + + let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite"); + function findContact(id) { + for (let child of parent.childCards) { + if (child.UID == id) { + return child; + } + } + return null; + } + function findMailingList(id) { + for (let list of parent.childNodes) { + if (list.UID == id) { + return list; + } + } + return null; + } + + extension.onMessage("outsideEventsTest", async (action, ...args) => { + switch (action) { + case "createAddressBook": { + let dirPrefId = MailServices.ab.newAddressBook( + "external add", + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + let book = MailServices.ab.getDirectoryFromId(dirPrefId); + extension.sendMessage(book.UID, dirPrefId); + return; + } + case "updateAddressBook": { + let book = MailServices.ab.getDirectoryFromId(args[0]); + book.dirName = "external edit"; + extension.sendMessage(); + return; + } + case "deleteAddressBook": { + let book = MailServices.ab.getDirectoryFromId(args[0]); + MailServices.ab.deleteAddressBook(book.URI); + extension.sendMessage(); + return; + } + case "createContact": { + let contact = new AddrBookCard(); + contact.firstName = "external"; + contact.lastName = "add"; + contact.primaryEmail = "test@invalid"; + + let newContact = parent.addCard(contact); + extension.sendMessage(parent.UID, newContact.UID); + return; + } + case "updateContact": { + let contact = findContact(args[0]); + if (contact) { + contact.firstName = "external"; + contact.lastName = "edit"; + parent.modifyCard(contact); + extension.sendMessage(); + return; + } + break; + } + case "deleteContact": { + let contact = findContact(args[0]); + if (contact) { + parent.deleteCards([contact]); + extension.sendMessage(); + return; + } + break; + } + case "createMailingList": { + let list = Cc[ + "@mozilla.org/addressbook/directoryproperty;1" + ].createInstance(Ci.nsIAbDirectory); + list.isMailList = true; + list.dirName = "external add"; + + let newList = parent.addMailList(list); + extension.sendMessage(parent.UID, newList.UID); + return; + } + case "updateMailingList": { + let list = findMailingList(args[0]); + if (list) { + list.dirName = "external edit"; + list.editMailListToDatabase(null); + extension.sendMessage(); + return; + } + break; + } + case "deleteMailingList": { + let list = findMailingList(args[0]); + if (list) { + parent.deleteDirectory(list); + extension.sendMessage(); + return; + } + break; + } + case "addMailingListMember": { + let list = findMailingList(args[0]); + let contact = findContact(args[1]); + + if (list && contact) { + list.addCard(contact); + equal(1, list.childCards.length); + extension.sendMessage(); + return; + } + break; + } + case "removeMailingListMember": { + let list = findMailingList(args[0]); + let contact = findContact(args[1]); + + if (list && contact) { + list.deleteCards([contact]); + equal(0, list.childCards.length); + ok(findContact(args[1]), "Contact was not removed"); + extension.sendMessage(); + return; + } + break; + } + } + throw new Error( + `Message "${action}" passed to handler didn't do anything.` + ); + }); + + await extension.startup(); + await extension.awaitFinish("addressBooks"); + await extension.unload(); +}); + +add_task(async function test_addressBooks_MV3_event_pages() { + await AddonTestUtils.promiseStartupManager(); + + let files = { + "background.js": async () => { + // Whenever the extension starts or wakes up, hasFired is set to false. In + // case of a wake-up, the first fired event is the one that woke up the background. + let hasFired = false; + + // Create and register event listener. + for (let event of [ + "addressBooks.onCreated", + "addressBooks.onUpdated", + "addressBooks.onDeleted", + "contacts.onCreated", + "contacts.onUpdated", + "contacts.onDeleted", + "mailingLists.onCreated", + "mailingLists.onUpdated", + "mailingLists.onDeleted", + "mailingLists.onMemberAdded", + "mailingLists.onMemberRemoved", + ]) { + let [apiName, eventName] = event.split("."); + browser[apiName][eventName].addListener((...args) => { + // Only send the first event after background wake-up, this should be + // the only one expected. + if (!hasFired) { + hasFired = true; + browser.test.sendMessage(`${apiName}.${eventName} received`, args); + } + }); + } + + browser.test.sendMessage("background started"); + }, + "utils.js": await getUtilsJS(), + }; + let extension = ExtensionTestUtils.loadExtension({ + files, + manifest: { + manifest_version: 3, + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["addressBooks"], + browser_specific_settings: { gecko: { id: "addressbook@xpcshell.test" } }, + }, + }); + + let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite"); + function findContact(id) { + for (let child of parent.childCards) { + if (child.UID == id) { + return child; + } + } + return null; + } + function findMailingList(id) { + for (let list of parent.childNodes) { + if (list.UID == id) { + return list; + } + } + return null; + } + function outsideEvent(action, ...args) { + switch (action) { + case "createAddressBook": { + let dirPrefId = MailServices.ab.newAddressBook( + "external add", + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + let book = MailServices.ab.getDirectoryFromId(dirPrefId); + return [book, dirPrefId]; + } + case "updateAddressBook": { + let book = MailServices.ab.getDirectoryFromId(args[0]); + book.dirName = "external edit"; + return []; + } + case "deleteAddressBook": { + let book = MailServices.ab.getDirectoryFromId(args[0]); + MailServices.ab.deleteAddressBook(book.URI); + return []; + } + case "createContact": { + let contact = new AddrBookCard(); + contact.firstName = "external"; + contact.lastName = "add"; + contact.primaryEmail = "test@invalid"; + + let newContact = parent.addCard(contact); + return [parent.UID, newContact.UID]; + } + case "updateContact": { + let contact = findContact(args[0]); + if (contact) { + contact.firstName = "external"; + contact.lastName = "edit"; + parent.modifyCard(contact); + return []; + } + break; + } + case "deleteContact": { + let contact = findContact(args[0]); + if (contact) { + parent.deleteCards([contact]); + return []; + } + break; + } + case "createMailingList": { + let list = Cc[ + "@mozilla.org/addressbook/directoryproperty;1" + ].createInstance(Ci.nsIAbDirectory); + list.isMailList = true; + list.dirName = "external add"; + + let newList = parent.addMailList(list); + return [parent.UID, newList.UID]; + } + case "updateMailingList": { + let list = findMailingList(args[0]); + if (list) { + list.dirName = "external edit"; + list.editMailListToDatabase(null); + return []; + } + break; + } + case "deleteMailingList": { + let list = findMailingList(args[0]); + if (list) { + parent.deleteDirectory(list); + return []; + } + break; + } + case "addMailingListMember": { + let list = findMailingList(args[0]); + let contact = findContact(args[1]); + + if (list && contact) { + list.addCard(contact); + equal(1, list.childCards.length); + return []; + } + break; + } + case "removeMailingListMember": { + let list = findMailingList(args[0]); + let contact = findContact(args[1]); + + if (list && contact) { + list.deleteCards([contact]); + equal(0, list.childCards.length); + ok(findContact(args[1]), "Contact was not removed"); + return []; + } + break; + } + } + throw new Error( + `Message "${action}" passed to handler didn't do anything.` + ); + } + + function checkPersistentListeners({ primed }) { + // A persistent event is referenced by its moduleName as defined in + // ext-mails.json, not by its actual namespace. + const persistent_events = [ + "addressBook.onAddressBookCreated", + "addressBook.onAddressBookUpdated", + "addressBook.onAddressBookDeleted", + "addressBook.onContactCreated", + "addressBook.onContactUpdated", + "addressBook.onContactDeleted", + "addressBook.onMailingListCreated", + "addressBook.onMailingListUpdated", + "addressBook.onMailingListDeleted", + "addressBook.onMemberAdded", + "addressBook.onMemberRemoved", + ]; + + for (let event of persistent_events) { + let [moduleName, eventName] = event.split("."); + assertPersistentListeners(extension, moduleName, eventName, { + primed, + }); + } + } + + await extension.startup(); + await extension.awaitMessage("background started"); + checkPersistentListeners({ primed: false }); + + // addressBooks.onCreated. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + let [newBook, dirPrefId] = outsideEvent("createAddressBook"); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [ + { + id: newBook.UID, + type: "addressBook", + name: "external add", + readOnly: false, + remote: false, + }, + ], + await extension.awaitMessage("addressBooks.onCreated received"), + "The primed addressBooks.onCreated event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // addressBooks.onUpdated. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("updateAddressBook", dirPrefId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [ + { + id: newBook.UID, + type: "addressBook", + name: "external edit", + readOnly: false, + remote: false, + }, + ], + await extension.awaitMessage("addressBooks.onUpdated received"), + "The primed addressBooks.onUpdated event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // addressBooks.onDeleted. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("deleteAddressBook", dirPrefId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [newBook.UID], + await extension.awaitMessage("addressBooks.onDeleted received"), + "The primed addressBooks.onDeleted event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // contacts.onCreated. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + let [parentId1, contactId] = outsideEvent("createContact"); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + let [createdNode] = await extension.awaitMessage( + "contacts.onCreated received" + ); + Assert.deepEqual( + { + type: "contact", + parentId: parentId1, + id: contactId, + }, + { + type: createdNode.type, + parentId: createdNode.parentId, + id: createdNode.id, + }, + "The primed contacts.onCreated event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // contacts.onUpdated. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("updateContact", contactId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + let [updatedNode, changedProperties] = await extension.awaitMessage( + "contacts.onUpdated received" + ); + Assert.deepEqual( + [ + { type: "contact", parentId: parentId1, id: contactId }, + { LastName: { oldValue: "add", newValue: "edit" } }, + ], + [ + { + type: updatedNode.type, + parentId: updatedNode.parentId, + id: updatedNode.id, + }, + changedProperties, + ], + "The primed contacts.onUpdated event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // mailingLists.onCreated. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + let [parentId2, listId] = outsideEvent("createMailingList"); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [ + { + type: "mailingList", + parentId: parentId2, + id: listId, + name: "external add", + nickName: "", + description: "", + readOnly: false, + remote: false, + }, + ], + await extension.awaitMessage("mailingLists.onCreated received"), + "The primed mailingLists.onCreated event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // mailingList.onUpdated. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("updateMailingList", listId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [ + { + type: "mailingList", + parentId: parentId2, + id: listId, + name: "external edit", + nickName: "", + description: "", + readOnly: false, + remote: false, + }, + ], + await extension.awaitMessage("mailingLists.onUpdated received"), + "The primed mailingLists.onUpdated event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // mailingList.onMemberAdded. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("addMailingListMember", listId, contactId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + let [addedNode] = await extension.awaitMessage( + "mailingLists.onMemberAdded received" + ); + Assert.deepEqual( + { type: "contact", parentId: listId, id: contactId }, + { type: addedNode.type, parentId: addedNode.parentId, id: addedNode.id }, + "The primed mailingLists.onMemberAdded event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // mailingList.onMemberRemoved. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("removeMailingListMember", listId, contactId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [listId, contactId], + await extension.awaitMessage("mailingLists.onMemberRemoved received"), + "The primed mailingLists.onMemberRemoved event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // mailingList.onDeleted. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("deleteMailingList", listId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [parentId2, listId], + await extension.awaitMessage("mailingLists.onDeleted received"), + "The primed mailingLists.onDeleted event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + // contacts.onDeleted. + + await extension.terminateBackground({ disableResetIdleForTest: true }); + checkPersistentListeners({ primed: true }); + outsideEvent("deleteContact", contactId); + // The event should have restarted the background. + await extension.awaitMessage("background started"); + Assert.deepEqual( + [parentId1, contactId], + await extension.awaitMessage("contacts.onDeleted received"), + "The primed contacts.onDeleted event should return the correct values" + ); + checkPersistentListeners({ primed: false }); + + await extension.unload(); + + await AddonTestUtils.promiseShutdownManager(); +}); + +add_task(async function test_photos() { + async function background() { + let events = []; + let eventPromise; + let eventPromiseResolve; + for (let eventNamespace of ["addressBooks", "contacts"]) { + for (let eventName of ["onCreated", "onUpdated", "onDeleted"]) { + if (eventName in browser[eventNamespace]) { + browser[eventNamespace][eventName].addListener((...args) => { + events.push({ namespace: eventNamespace, name: eventName, args }); + if (eventPromiseResolve) { + let resolve = eventPromiseResolve; + eventPromiseResolve = null; + resolve(); + } + }); + } + } + } + + let getDataUrl = function (file) { + return new Promise((resolve, reject) => { + var reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = function () { + resolve(reader.result); + }; + reader.onerror = function (error) { + reject(new Error(error)); + }; + }); + }; + + let updateAndVerifyPhoto = async function ( + parentId, + id, + photoFile, + photoData + ) { + eventPromise = new Promise(resolve => { + eventPromiseResolve = resolve; + }); + await browser.contacts.setPhoto(id, photoFile); + + await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId, id }, + {}, + ]); + let updatedPhoto = await browser.contacts.getPhoto(id); + // eslint-disable-next-line mozilla/use-isInstance + browser.test.assertTrue(updatedPhoto instanceof File); + browser.test.assertEq("image/png", updatedPhoto.type); + browser.test.assertEq(`${id}.png`, updatedPhoto.name); + browser.test.assertEq(photoData, await getDataUrl(updatedPhoto)); + }; + let normalizeVCard = function (vCard) { + return vCard + .replaceAll("\r\n", "") + .replaceAll("\n", "") + .replaceAll(" ", ""); + }; + let outsideEvent = function (action, ...args) { + eventPromise = new Promise(resolve => { + eventPromiseResolve = resolve; + }); + return window.sendMessage("outsideEventsTest", action, ...args); + }; + let checkEvents = async function (...expectedEvents) { + if (eventPromiseResolve) { + await eventPromise; + } + + browser.test.assertEq( + expectedEvents.length, + events.length, + "Correct number of events" + ); + + if (expectedEvents.length != events.length) { + for (let event of events) { + let args = event.args.join(", "); + browser.test.log(`${event.namespace}.${event.name}(${args})`); + } + throw new Error("Wrong number of events, stopping."); + } + + for (let [namespace, name, ...expectedArgs] of expectedEvents) { + let event = events.shift(); + browser.test.assertEq( + namespace, + event.namespace, + "Event namespace is correct" + ); + browser.test.assertEq(name, event.name, "Event type is correct"); + browser.test.assertEq( + expectedArgs.length, + event.args.length, + "Argument count is correct" + ); + window.assertDeepEqual(expectedArgs, event.args); + if (expectedEvents.length == 1) { + return event.args; + } + } + + return null; + }; + + let whitePixelData = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"; + let bluePixelData = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg=="; + let greenPixelData = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY5CeYAMAAbEA6ASxSWcAAAAASUVORK5CYII="; + let redPixelData = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY3growIAAycBLhVrvukAAAAASUVORK5CYII="; + let vCard3WhitePixel = + "PHOTO;ENCODING=B;TYPE=PNG:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"; + let vCard4WhitePixel = + "PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC"; + let vCard4BluePixel = + "PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg=="; + + // Create a photo file, which is linked to a local file to simulate a file + // opened through a filepicker. + let [redPixelRealFile] = await window.sendMessage("getRedPixelFile"); + + // Create a photo file, which is a simple data blob. + let greenPixelFile = await fetch(greenPixelData) + .then(res => res.arrayBuffer()) + .then(buf => new File([buf], "greenPixel.png", { type: "image/png" })); + + // ------------------------------------------------------------------------- + // Test vCard v4 with a photoName set. + // ------------------------------------------------------------------------- + + let [parentId1, contactId1, photoName1] = await outsideEvent( + "createV4ContactWithPhotoName" + ); + let [newContact] = await checkEvents([ + "contacts", + "onCreated", + { type: "contact", parentId: parentId1, id: contactId1 }, + ]); + browser.test.assertEq("external", newContact.properties.FirstName); + browser.test.assertEq("add", newContact.properties.LastName); + browser.test.assertTrue( + newContact.properties.vCard.includes("VERSION:4.0"), + "vCard should be version 4.0" + ); + browser.test.assertTrue( + normalizeVCard(newContact.properties.vCard).includes(vCard4WhitePixel), + `vCard should include the correct Photo property [${normalizeVCard( + newContact.properties.vCard + )}] vs [${vCard4WhitePixel}]` + ); + // Check internal photoUrl is the correct fileUrl. + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId1, + `^file:.*?${photoName1}$` + ); + + // Test if we can get the photo through the API. + + let photo = await browser.contacts.getPhoto(contactId1); + // eslint-disable-next-line mozilla/use-isInstance + browser.test.assertTrue(photo instanceof File); + browser.test.assertEq("image/png", photo.type); + browser.test.assertEq(`${contactId1}.png`, photo.name); + browser.test.assertEq( + whitePixelData, + await getDataUrl(photo), + "vCard 4.0 contact with photo from internal fileUrl from photoName should return the correct photo file" + ); + // Re-check internal photoUrl. + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId1, + `^file:.*?${photoName1}$` + ); + + // Test if we can update the photo through the API by providing a file which + // is linked to a local file. Since this vCard had only a photoName set and + // its photo stored as a local file, the updated photo should also be stored + // as a local file. + + await updateAndVerifyPhoto( + parentId1, + contactId1, + redPixelRealFile, + redPixelData + ); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId1, + `^file:.*?${contactId1}\.png$` + ); + + // Test if we can update the photo through the API, by providing a pure data + // blob (decoupled from a local file, without file.mozFullPath set). + + await updateAndVerifyPhoto( + parentId1, + contactId1, + greenPixelFile, + greenPixelData + ); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId1, + `^file:.*?${contactId1}-1\.png$` + ); + + // Test if we get the correct photo if it is updated by the user, storing the + // photo in its vCard (outside of the API). + + await outsideEvent("updateV4ContactWithBluePixel", contactId1); + let [updatedContact1] = await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: parentId1, id: contactId1 }, + { LastName: { oldValue: "add", newValue: "edit" } }, + ]); + browser.test.assertEq("external", updatedContact1.properties.FirstName); + browser.test.assertEq("edit", updatedContact1.properties.LastName); + let updatedPhoto1 = await browser.contacts.getPhoto(contactId1); + // eslint-disable-next-line mozilla/use-isInstance + browser.test.assertTrue(updatedPhoto1 instanceof File); + browser.test.assertEq("image/png", updatedPhoto1.type); + browser.test.assertEq(`${contactId1}.png`, updatedPhoto1.name); + browser.test.assertEq(bluePixelData, await getDataUrl(updatedPhoto1)); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId1, + bluePixelData + ); + + // ------------------------------------------------------------------------- + // Test vCard v4 with a photoName and also a photo in its vCard. + // ------------------------------------------------------------------------- + + let [parentId2, contactId2] = await outsideEvent( + "createV4ContactWithBothPhotoProps" + ); + let [newContact2] = await checkEvents([ + "contacts", + "onCreated", + { type: "contact", parentId: parentId2, id: contactId2 }, + ]); + browser.test.assertEq("external", newContact2.properties.FirstName); + browser.test.assertEq("add", newContact2.properties.LastName); + browser.test.assertTrue( + newContact2.properties.vCard.includes("VERSION:4.0"), + "vCard should be version 4.0" + ); + // The card should not include vCard4WhitePixel (which photoName points to), + // but the value of vCard4BluePixel stored in the vCard photo property. + browser.test.assertTrue( + normalizeVCard(newContact2.properties.vCard).includes(vCard4BluePixel), + `vCard should include the correct Photo property [${normalizeVCard( + newContact2.properties.vCard + )}] vs [${vCard4BluePixel}]` + ); + // Check internal photoUrl is the correct dataUrl. + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId2, + bluePixelData + ); + + // Test if we can get the correct photo through the API. + + let photo3 = await browser.contacts.getPhoto(contactId2); + // eslint-disable-next-line mozilla/use-isInstance + browser.test.assertTrue(photo3 instanceof File); + browser.test.assertEq("image/png", photo3.type); + browser.test.assertEq(`${contactId2}.png`, photo3.name); + browser.test.assertEq( + bluePixelData, + await getDataUrl(photo3), + "vCard 4.0 contact with photo from internal dataUrl from vCard (vCard wins over photoName) should return the correct photo file" + ); + // Re-check internal photoUrl. + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId2, + bluePixelData + ); + + // Test if we can update the photo through the API by providing a file which + // is linked to a local file. Since this vCard had its photo stored as dataUrl + // in the vCard, the updated photo should be stored as a dataUrl as well. + + await updateAndVerifyPhoto( + parentId2, + contactId2, + redPixelRealFile, + redPixelData + ); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId2, + redPixelData + ); + + // Test if we can update the photo through the API, by providing a pure data + // blob (decoupled from a local file, without file.mozFullPath set). + + await updateAndVerifyPhoto( + parentId2, + contactId2, + greenPixelFile, + greenPixelData + ); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId2, + greenPixelData + ); + + // ------------------------------------------------------------------------- + // Test vCard v3 with a photoName set. + // ------------------------------------------------------------------------- + + let [parentId3, contactId3, photoName4] = await outsideEvent( + "createV3ContactWithPhotoName" + ); + let [newContact4] = await checkEvents([ + "contacts", + "onCreated", + { type: "contact", parentId: parentId3, id: contactId3 }, + ]); + browser.test.assertEq("external", newContact4.properties.FirstName); + browser.test.assertEq("add", newContact4.properties.LastName); + browser.test.assertTrue( + newContact4.properties.vCard.includes("VERSION:3.0"), + "vCard should be version 3.0" + ); + browser.test.assertTrue( + normalizeVCard(newContact4.properties.vCard).includes(vCard3WhitePixel), + `vCard should include the correct Photo property [${normalizeVCard( + newContact4.properties.vCard + )}] vs [${vCard3WhitePixel}]` + ); + // Check internal photoUrl is the correct fileUrl. + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId3, + `^file:.*?${photoName4}$` + ); + let photo4 = await browser.contacts.getPhoto(contactId3); + // eslint-disable-next-line mozilla/use-isInstance + browser.test.assertTrue(photo4 instanceof File); + browser.test.assertEq("image/png", photo4.type); + browser.test.assertEq(`${contactId3}.png`, photo4.name); + browser.test.assertEq( + whitePixelData, + await getDataUrl(photo4), + "vCard 3.0 contact with photo from internal fileUrl from photoName should return the correct photo file" + ); + // Re-check internal photoUrl. + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId3, + `^file:.*?${photoName4}$` + ); + + // Test if we can update the photo through the API by providing a file which + // is linked to a local file. Since this vCard had only a photoName set and + // its photo stored as a local file, the updated photo should also be stored + // as a local file. + + await updateAndVerifyPhoto( + parentId3, + contactId3, + redPixelRealFile, + redPixelData + ); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId3, + `^file:.*?${contactId3}\.png$` + ); + + // Test if we can update the photo through the API, by providing a pure data + // blob (decoupled from a local file, without file.mozFullPath set). + + await updateAndVerifyPhoto( + parentId3, + contactId3, + greenPixelFile, + greenPixelData + ); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId3, + `^file:.*?${contactId3}-1\.png$` + ); + + // Test if we get the correct photo if it is updated by the user, storing the + // photo in its vCard (outside of the API). + + await outsideEvent("updateV3ContactWithBluePixel", contactId3); + let [updatedContact3] = await checkEvents([ + "contacts", + "onUpdated", + { type: "contact", parentId: parentId3, id: contactId3 }, + { LastName: { oldValue: "add", newValue: "edit" } }, + ]); + browser.test.assertEq("external", updatedContact3.properties.FirstName); + browser.test.assertEq("edit", updatedContact3.properties.LastName); + let updatedPhoto3 = await browser.contacts.getPhoto(contactId3); + // eslint-disable-next-line mozilla/use-isInstance + browser.test.assertTrue(updatedPhoto3 instanceof File); + browser.test.assertEq("image/png", updatedPhoto3.type); + browser.test.assertEq(`${contactId3}.png`, updatedPhoto3.name); + browser.test.assertEq(bluePixelData, await getDataUrl(updatedPhoto3)); + await window.sendMessage( + "verifyInternalPhotoUrl", + contactId3, + bluePixelData + ); + + // Cleanup. Delete all created contacts. + + await outsideEvent("deleteContact", contactId1); + await checkEvents(["contacts", "onDeleted", parentId1, contactId1]); + await outsideEvent("deleteContact", contactId2); + await checkEvents(["contacts", "onDeleted", parentId2, contactId2]); + await outsideEvent("deleteContact", contactId3); + await checkEvents(["contacts", "onDeleted", parentId3, contactId3]); + browser.test.notifyPass("addressBooksPhotos"); + } + + let extension = ExtensionTestUtils.loadExtension({ + files: { + "background.js": background, + "utils.js": await getUtilsJS(), + }, + manifest: { + background: { scripts: ["utils.js", "background.js"] }, + permissions: ["addressBooks"], + }, + }); + + let parent = MailServices.ab.getDirectory("jsaddrbook://abook.sqlite"); + function findContact(id) { + for (let child of parent.childCards) { + if (child.UID == id) { + return child; + } + } + return null; + } + + async function getUniqueWhitePixelFile() { + // Copy photo file into the required Photos subfolder of the profile folder. + let photoName = `${AddrBookUtils.newUID()}.png`; + await IOUtils.copy( + do_get_file("images/whitePixel.png").path, + PathUtils.join(PathUtils.profileDir, "Photos", photoName) + ); + return photoName; + } + + extension.onMessage("getRedPixelFile", async () => { + let redPixelFile = await File.createFromNsIFile( + do_get_file("images/redPixel.png") + ); + extension.sendMessage(redPixelFile); + }); + + extension.onMessage("verifyInternalPhotoUrl", (id, expected) => { + let contact = findContact(id); + let photoUrl = contact.photoURL; + if (expected.startsWith("data:")) { + Assert.equal(expected, photoUrl, `photoURL should be correct`); + } else { + let regExp = new RegExp(expected); + Assert.ok( + regExp.test(photoUrl), + `photoURL <${photoUrl}> should match expected regExp <${expected}>` + ); + } + extension.sendMessage(); + }); + + extension.onMessage("outsideEventsTest", async (action, ...args) => { + switch (action) { + case "createV4ContactWithPhotoName": { + let photoName = await getUniqueWhitePixelFile(); + let contact = new AddrBookCard(); + contact.firstName = "external"; + contact.lastName = "add"; + contact.primaryEmail = "test@invalid"; + contact.setProperty("PhotoName", photoName); + + let newContact = parent.addCard(contact); + extension.sendMessage(parent.UID, newContact.UID, photoName); + return; + } + case "createV4ContactWithBothPhotoProps": { + // This contact has whitePixel as file but bluePixel in the vCard. + let photoName = await getUniqueWhitePixelFile(); + let contact = new AddrBookCard(); + contact.setProperty("PhotoName", photoName); + contact.setProperty( + "_vCard", + formatVCard` + BEGIN:VCARD + VERSION:4.0 + EMAIL;PREF=1:test@invalid + N:add;external;;; + UID:fd9aecf9-2453-4ba1-bec6-574a15bb380b + PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAA + ACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg== + END:VCARD + ` + ); + let newContact = parent.addCard(contact); + extension.sendMessage(parent.UID, newContact.UID, photoName); + return; + } + case "updateV4ContactWithBluePixel": { + let contact = findContact(args[0]); + if (contact) { + contact.setProperty( + "_vCard", + formatVCard` + BEGIN:VCARD + VERSION:4.0 + EMAIL;PREF=1:test@invalid + N:edit;external;;; + PHOTO;VALUE=URL:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAA + ACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg== + END:VCARD + ` + ); + parent.modifyCard(contact); + extension.sendMessage(); + return; + } + break; + } + case "createV3ContactWithPhotoName": { + let photoName = await getUniqueWhitePixelFile(); + let contact = new AddrBookCard(); + contact.setProperty("PhotoName", photoName); + contact.setProperty( + "_vCard", + formatVCard` + BEGIN:VCARD + VERSION:3.0 + EMAIL:test@invalid + N:add;external + UID:fd9aecf9-2453-4ba1-bec6-574a15bb380c + END:VCARD + ` + ); + let newContact = parent.addCard(contact); + extension.sendMessage(parent.UID, newContact.UID, photoName); + return; + } + case "updateV3ContactWithBluePixel": { + let contact = findContact(args[0]); + if (contact) { + contact.setProperty( + "_vCard", + formatVCard` + BEGIN:VCARD + VERSION:3.0 + EMAIL:test@invalid + N:edit;external + PHOTO;ENCODING=b;TYPE=PNG:iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAD + ElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQg== + END:VCARD + ` + ); + parent.modifyCard(contact); + extension.sendMessage(); + return; + } + break; + } + case "deleteContact": { + let contact = findContact(args[0]); + if (contact) { + parent.deleteCards([contact]); + extension.sendMessage(); + return; + } + break; + } + } + throw new Error( + `Message "${action}" passed to handler didn't do anything.` + ); + }); + + await extension.startup(); + await extension.awaitFinish("addressBooksPhotos"); + await extension.unload(); +}); |