summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/addrbook/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mailnews/addrbook/test/CardDAVServer.jsm634
-rw-r--r--comm/mailnews/addrbook/test/LDAPServer.jsm324
-rw-r--r--comm/mailnews/addrbook/test/moz.build14
-rw-r--r--comm/mailnews/addrbook/test/unit/data/bug534822prefs.js7
-rw-r--r--comm/mailnews/addrbook/test/unit/data/cardForEmail.sql95
-rw-r--r--comm/mailnews/addrbook/test/unit/data/collect.sql21
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.csv4
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.ldif36
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.txt4
-rw-r--r--comm/mailnews/addrbook/test/unit/data/export.vcf20
-rw-r--r--comm/mailnews/addrbook/test/unit/data/ldap_contacts.json104
-rw-r--r--comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat17
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf101
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf102
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf102
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf102
-rw-r--r--comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf204
-rw-r--r--comm/mailnews/addrbook/test/unit/head.js66
-rw-r--r--comm/mailnews/addrbook/test/unit/head_cardDAV.js149
-rw-r--r--comm/mailnews/addrbook/test/unit/test_LDAPMessage.js101
-rw-r--r--comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js66
-rw-r--r--comm/mailnews/addrbook/test/unit/test_abCardProperty.js178
-rw-r--r--comm/mailnews/addrbook/test/unit/test_addrBookCard.js260
-rw-r--r--comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js125
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug1522453.js72
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug1769889.js95
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug387403.js16
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug448165.js18
-rw-r--r--comm/mailnews/addrbook/test/unit/test_bug534822.js38
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js148
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js550
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js68
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js282
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js408
-rw-r--r--comm/mailnews/addrbook/test/unit/test_cardForEmail.js111
-rw-r--r--comm/mailnews/addrbook/test/unit/test_collection.js404
-rw-r--r--comm/mailnews/addrbook/test/unit/test_collection_2.js42
-rw-r--r--comm/mailnews/addrbook/test/unit/test_convertOnSave.js329
-rw-r--r--comm/mailnews/addrbook/test/unit/test_db_enumerator.js89
-rw-r--r--comm/mailnews/addrbook/test/unit/test_delete_book.js82
-rw-r--r--comm/mailnews/addrbook/test/unit/test_export.js156
-rw-r--r--comm/mailnews/addrbook/test/unit/test_jsaddrbook.js420
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldap1.js205
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldap2.js41
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldapOffline.js47
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldapReplication.js159
-rw-r--r--comm/mailnews/addrbook/test/unit/test_ldapquery.js181
-rw-r--r--comm/mailnews/addrbook/test/unit/test_mailList1.js65
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js128
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js468
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js194
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js164
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js258
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js120
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js248
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js162
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager2.js83
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager3.js42
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager4.js75
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager5.js43
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsAbManager6.js27
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js40
-rw-r--r--comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js428
-rw-r--r--comm/mailnews/addrbook/test/unit/test_photoURL.js35
-rw-r--r--comm/mailnews/addrbook/test/unit/test_preferDisplayName.js79
-rw-r--r--comm/mailnews/addrbook/test/unit/test_search.js65
-rw-r--r--comm/mailnews/addrbook/test/unit/test_vCard.js474
-rw-r--r--comm/mailnews/addrbook/test/unit/test_vCard21.js190
-rw-r--r--comm/mailnews/addrbook/test/unit/test_vCardProperties.js899
-rw-r--r--comm/mailnews/addrbook/test/unit/xpcshell.ini60
-rw-r--r--comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini12
74 files changed, 11768 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/test/CardDAVServer.jsm b/comm/mailnews/addrbook/test/CardDAVServer.jsm
new file mode 100644
index 0000000000..5bd1275b41
--- /dev/null
+++ b/comm/mailnews/addrbook/test/CardDAVServer.jsm
@@ -0,0 +1,634 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["CardDAVServer"];
+
+const PREFIX_BINDINGS = {
+ card: "urn:ietf:params:xml:ns:carddav",
+ cs: "http://calendarserver.org/ns/",
+ d: "DAV:",
+};
+const NAMESPACE_STRING = Object.entries(PREFIX_BINDINGS)
+ .map(([prefix, url]) => `xmlns:${prefix}="${url}"`)
+ .join(" ");
+
+const { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+const { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+var CardDAVServer = {
+ books: {
+ "/addressbooks/me/default/": "Not This One",
+ "/addressbooks/me/test/": "CardDAV Test",
+ },
+ cards: new Map(),
+ movedCards: new Map(),
+ deletedCards: new Map(),
+ changeCount: 0,
+ server: null,
+ isOpen: false,
+
+ open(username, password, port = -1) {
+ this.server = new HttpServer();
+ this.server.start(port);
+ this.port = this.server.identity.primaryPort;
+ this.isOpen = true;
+
+ this.username = username;
+ this.password = password;
+ this.server.registerPathHandler("/ping", this.ping);
+
+ this.reset();
+ },
+
+ reopen() {
+ this.server.start(this.port);
+ this.isOpen = true;
+ },
+
+ reset() {
+ this.cards.clear();
+ this.deletedCards.clear();
+ this.changeCount = 0;
+ this.resetHandlers();
+ },
+
+ resetHandlers() {
+ // Address book discovery.
+
+ this.server.registerPathHandler("/", this.wellKnown.bind(this));
+ this.server.registerPathHandler(
+ "/.well-known/carddav",
+ this.wellKnown.bind(this)
+ );
+ this.server.registerPathHandler("/principals/", this.principals.bind(this));
+ this.server.registerPathHandler(
+ "/principals/me/",
+ this.myPrincipal.bind(this)
+ );
+ this.server.registerPathHandler(
+ "/addressbooks/me/",
+ this.myAddressBooks.bind(this)
+ );
+
+ // Address book interaction.
+
+ for (let path of Object.keys(this.books)) {
+ this.server.registerPathHandler(path, this.directoryHandler.bind(this));
+ this.server.registerPrefixHandler(path, this.cardHandler.bind(this));
+ }
+ },
+
+ close() {
+ if (!this.isOpen) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve =>
+ this.server.stop({
+ onStopped: () => {
+ this.isOpen = false;
+ resolve();
+ },
+ })
+ );
+ },
+
+ get origin() {
+ return `http://localhost:${this.server.identity.primaryPort}`;
+ },
+
+ get path() {
+ return "/addressbooks/me/test/";
+ },
+
+ get url() {
+ return `${this.origin}${this.path}`;
+ },
+
+ get altPath() {
+ return "/addressbooks/me/default/";
+ },
+
+ get altURL() {
+ return `${this.origin}${this.altPath}`;
+ },
+
+ checkAuth(request, response) {
+ if (!this.username || !this.password) {
+ return true;
+ }
+ if (!request.hasHeader("Authorization")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return false;
+ }
+
+ let value = request.getHeader("Authorization");
+ if (!value.startsWith("Basic ")) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return false;
+ }
+
+ let [username, password] = atob(value.substring(6)).split(":");
+ if (username != this.username || password != this.password) {
+ response.setStatusLine("1.1", 401, "Unauthorized");
+ response.setHeader("WWW-Authenticate", `Basic realm="test"`);
+ return false;
+ }
+
+ return true;
+ },
+
+ ping(request, response) {
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Content-Type", "text/plain");
+ response.write("pong");
+ },
+
+ wellKnown(request, response) {
+ response.setStatusLine("1.1", 301, "Moved Permanently");
+ response.setHeader("Location", "/principals/");
+ },
+
+ principals(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ let propNames = this._inputProps(input);
+ let propValues = {
+ "d:current-user-principal": "<href>/principals/me/</href>",
+ };
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(
+ `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>/principals/</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>
+ </multistatus>`.replace(/>\s+</g, "><")
+ );
+ },
+
+ myPrincipal(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ let propNames = this._inputProps(input);
+ let propValues = {
+ "d:resourcetype": "<principal/>",
+ "card:addressbook-home-set": "<href>/addressbooks/me/</href>",
+ };
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(
+ `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>/principals/me/</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>
+ </multistatus>`.replace(/>\s+</g, "><")
+ );
+ },
+
+ myAddressBooks(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ let propNames = this._inputProps(input);
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>/addressbooks/me/</href>
+ ${this._outputProps(propNames, {
+ "d:resourcetype": "<collection/>",
+ "d:displayname": "#addressbooks",
+ })}
+ </response>`;
+
+ for (let [path, name] of Object.entries(this.books)) {
+ output += `<response>
+ <href>${path}</href>
+ ${this._outputProps(propNames, {
+ "d:resourcetype": "<collection/><card:addressbook/>",
+ "d:displayname": name,
+ "d:current-user-privilege-set":
+ "<d:privilege><d:all/></d:privilege>",
+ })}
+ </response>`;
+ }
+
+ output += `</multistatus>`;
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ /** Handle any requests to the address book itself. */
+
+ directoryHandler(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let isRealDirectory = request.path == this.path;
+ let input = new DOMParser().parseFromString(
+ CommonUtils.readBytesFromInputStream(request.bodyInputStream),
+ "text/xml"
+ );
+
+ switch (input.documentElement.localName) {
+ case "addressbook-query":
+ Assert.equal(request.method, "REPORT");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.card);
+ this.addressBookQuery(input, response, isRealDirectory);
+ return;
+ case "addressbook-multiget":
+ Assert.equal(request.method, "REPORT");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.card);
+ this.addressBookMultiGet(input, response, isRealDirectory);
+ return;
+ case "propfind":
+ Assert.equal(request.method, "PROPFIND");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d);
+ this.propFind(
+ input,
+ request.hasHeader("Depth") ? request.getHeader("Depth") : 0,
+ response,
+ isRealDirectory
+ );
+ return;
+ case "sync-collection":
+ Assert.equal(request.method, "REPORT");
+ Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d);
+ this.syncCollection(input, response, isRealDirectory);
+ return;
+ }
+
+ Assert.report(true, undefined, undefined, "Should not have reached here");
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`No handler found for <${input.documentElement.localName}>`);
+ },
+
+ addressBookQuery(input, response, isRealDirectory) {
+ if (this.mimicYahoo) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+
+ let propNames = this._inputProps(input);
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
+ if (isRealDirectory) {
+ for (let [href, card] of this.cards) {
+ output += this._cardResponse(href, card, propNames);
+ }
+ }
+ output += `</multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ addressBookMultiGet(input, response, isRealDirectory) {
+ let propNames = this._inputProps(input);
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
+ if (isRealDirectory) {
+ for (let href of input.querySelectorAll("href")) {
+ href = href.textContent;
+ if (this.movedCards.has(href)) {
+ href = this.movedCards.get(href);
+ }
+ let card = this.cards.get(href);
+ if (card) {
+ output += this._cardResponse(href, card, propNames);
+ }
+ }
+ }
+ output += `</multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ propFind(input, depth, response, isRealDirectory) {
+ let propNames = this._inputProps(input);
+
+ if (this.mimicYahoo && !propNames.includes("cs:getctag")) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+
+ let propValues = {
+ "cs:getctag": this.changeCount,
+ "d:displayname": isRealDirectory ? "CardDAV Test" : "Not This One",
+ "d:resourcetype": "<collection/><card:addressbook/>",
+ "d:current-user-privilege-set": "<d:privilege><d:all/></d:privilege>",
+ };
+ if (!this.mimicYahoo) {
+ propValues["d:sync-token"] = `http://mochi.test/sync/${this.changeCount}`;
+ }
+
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>
+ <response>
+ <href>${isRealDirectory ? this.path : this.altPath}</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>`;
+ if (depth == 1 && isRealDirectory) {
+ for (let [href, card] of this.cards) {
+ output += this._cardResponse(href, card, propNames);
+ }
+ }
+ output += `</multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ syncCollection(input, response, isRealDirectory) {
+ let token = input
+ .querySelector("sync-token")
+ .textContent.replace(/\D/g, "");
+ if (!token) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+ let propNames = this._inputProps(input);
+
+ let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`;
+ if (isRealDirectory) {
+ for (let [href, card] of this.cards) {
+ if (card.changed > token) {
+ output += this._cardResponse(
+ href,
+ card,
+ propNames,
+ !this.mimicGoogle
+ );
+ }
+ }
+ for (let [href, deleted] of this.deletedCards) {
+ if (deleted > token) {
+ output += `<response>
+ <status>HTTP/1.1 404 Not Found</status>
+ <href>${href}</href>
+ <propstat>
+ <prop/>
+ <status>HTTP/1.1 418 I'm a teapot</status>
+ </propstat>
+ </response>`;
+ }
+ }
+ }
+ output += `<sync-token>http://mochi.test/sync/${this.changeCount}</sync-token>
+ </multistatus>`;
+
+ response.setStatusLine("1.1", 207, "Multi-Status");
+ response.setHeader("Content-Type", "text/xml");
+ response.write(output.replace(/>\s+</g, "><"));
+ },
+
+ _cardResponse(href, card, propNames, includeAddressData = true) {
+ let propValues = {
+ "d:getetag": card.etag,
+ "d:resourcetype": null,
+ };
+
+ if (includeAddressData) {
+ propValues["card:address-data"] = card.vCard;
+ }
+
+ let outString = `<response>
+ <href>${href}</href>
+ ${this._outputProps(propNames, propValues)}
+ </response>`;
+ return outString;
+ },
+
+ _inputProps(input) {
+ let props = input.querySelectorAll("prop > *");
+ let propNames = [];
+
+ for (let p of props) {
+ Assert.equal(p.childElementCount, 0);
+ switch (p.localName) {
+ case "address-data":
+ case "addressbook-home-set":
+ Assert.equal(p.namespaceURI, PREFIX_BINDINGS.card);
+ propNames.push(`card:${p.localName}`);
+ break;
+ case "getctag":
+ Assert.equal(p.namespaceURI, PREFIX_BINDINGS.cs);
+ propNames.push(`cs:${p.localName}`);
+ break;
+ case "current-user-principal":
+ case "current-user-privilege-set":
+ case "displayname":
+ case "getetag":
+ case "resourcetype":
+ case "sync-token":
+ Assert.equal(p.namespaceURI, PREFIX_BINDINGS.d);
+ propNames.push(`d:${p.localName}`);
+ break;
+ default:
+ Assert.report(
+ true,
+ undefined,
+ undefined,
+ `Unknown property requested: ${p.nodeName}`
+ );
+ break;
+ }
+ }
+
+ return propNames;
+ },
+
+ _outputProps(propNames, propValues) {
+ let output = "";
+
+ let found = [];
+ let notFound = [];
+ for (let p of propNames) {
+ if (p in propValues && propValues[p] !== undefined) {
+ found.push(`<${p}>${propValues[p]}</${p}>`);
+ } else {
+ notFound.push(`<${p}/>`);
+ }
+ }
+
+ if (found.length > 0) {
+ output += `<propstat>
+ <prop>
+ ${found.join("\n")}
+ </prop>
+ <status>HTTP/1.1 200 OK</status>
+ </propstat>`;
+ }
+ if (notFound.length > 0) {
+ output += `<propstat>
+ <prop>
+ ${notFound.join("\n")}
+ </prop>
+ <status>HTTP/1.1 404 Not Found</status>
+ </propstat>`;
+ }
+
+ return output;
+ },
+
+ /** Handle any requests to address book cards. */
+
+ cardHandler(request, response) {
+ if (!this.checkAuth(request, response)) {
+ return;
+ }
+
+ let isRealDirectory = request.path.startsWith(this.path);
+ if (!isRealDirectory || !/\/[\w-]+\.vcf$/.test(request.path)) {
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`Card not found at ${request.path}`);
+ return;
+ }
+
+ switch (request.method) {
+ case "GET":
+ this.getCard(request, response);
+ return;
+ case "PUT":
+ this.putCard(request, response);
+ return;
+ case "DELETE":
+ this.deleteCard(request, response);
+ return;
+ }
+
+ Assert.report(true, undefined, undefined, "Should not have reached here");
+ response.setStatusLine("1.1", 405, "Method Not Allowed");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`Method not allowed: ${request.method}`);
+ },
+
+ getCard(request, response) {
+ let card = this.cards.get(request.path);
+ if (!card) {
+ response.setStatusLine("1.1", 404, "Not Found");
+ response.setHeader("Content-Type", "text/plain");
+ response.write(`Card not found at ${request.path}`);
+ return;
+ }
+
+ response.setStatusLine("1.1", 200, "OK");
+ response.setHeader("Content-Type", "text/vcard");
+ response.setHeader("ETag", card.etag);
+ response.write(card.vCard);
+ },
+
+ putCard(request, response) {
+ if (request.hasHeader("If-Match")) {
+ let card = this.cards.get(request.path);
+ if (!card || card.etag != request.getHeader("If-Match")) {
+ response.setStatusLine("1.1", 412, "Precondition Failed");
+ return;
+ }
+ }
+
+ let vCard = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+ if (this.mimicGoogle && !/^N[;:]/im.test(vCard)) {
+ response.setStatusLine("1.1", 400, "Bad Request");
+ return;
+ }
+
+ this.putCardInternal(request.path, vCard);
+ response.setStatusLine("1.1", 204, "No Content");
+
+ if (this.responseDelay) {
+ response.processAsync();
+ this.responseDelay.promise.then(() => {
+ delete this.responseDelay;
+ response.finish();
+ });
+ }
+ },
+
+ putCardInternal(name, vCard) {
+ if (!name.startsWith("/")) {
+ name = this.path + name;
+ }
+ if (this.modifyCardOnPut && !this.cards.has(name)) {
+ vCard = vCard.replace(/UID:(\S+)/, (match, uid) => {
+ let newUID = [...uid].reverse().join("");
+ let newName = this.path + newUID + ".vcf";
+ this.movedCards.set(name, newName);
+ name = newName;
+ return "UID:" + newUID + "\r\nX-MODIFIED-BY-SERVER:1";
+ });
+ }
+ if (this.mimicGoogle && vCard.includes("\nPHOTO")) {
+ let [, version] = vCard.match(/VERSION:([34]\.0)/);
+ if (version && version != "3.0") {
+ let start = vCard.indexOf("\nPHOTO") + 1;
+ let end = vCard.indexOf("\n", start) + 1;
+ while (vCard[end] == " ") {
+ end = vCard.indexOf("\n", end) + 1;
+ }
+ vCard = vCard.substring(0, start) + vCard.substring(end);
+ }
+ }
+ let etag = "" + vCard.length;
+ this.cards.set(name, { etag, vCard, changed: ++this.changeCount });
+ this.deletedCards.delete(name);
+ },
+
+ deleteCard(request, response) {
+ this.deleteCardInternal(request.path);
+ response.setStatusLine("1.1", 204, "No Content");
+
+ if (this.responseDelay) {
+ response.processAsync();
+ this.responseDelay.promise.then(() => {
+ delete this.responseDelay;
+ response.finish();
+ });
+ }
+ },
+
+ deleteCardInternal(name) {
+ if (!name.startsWith("/")) {
+ name = this.path + name;
+ }
+ this.cards.delete(name);
+ this.deletedCards.set(name, ++this.changeCount);
+ },
+};
diff --git a/comm/mailnews/addrbook/test/LDAPServer.jsm b/comm/mailnews/addrbook/test/LDAPServer.jsm
new file mode 100644
index 0000000000..c8d8edb82b
--- /dev/null
+++ b/comm/mailnews/addrbook/test/LDAPServer.jsm
@@ -0,0 +1,324 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["LDAPServer"];
+const PRINT_DEBUG = false;
+
+const { Assert } = ChromeUtils.importESModule(
+ "resource://testing-common/Assert.sys.mjs"
+);
+
+/**
+ * This is a partial implementation of an LDAP server as defined by RFC 4511.
+ * It's not intended to serve any particular dataset, rather, tests should
+ * cause the application to make requests and tell the server what to respond.
+ *
+ * https://docs.ldap.com/specs/rfc4511.txt
+ *
+ * @implements {nsIInputStreamCallback}
+ * @implements {nsIServerSocketListener}
+ */
+var LDAPServer = {
+ BindRequest: 0x60,
+ UnbindRequest: 0x42,
+ SearchRequest: 0x63,
+ AbandonRequest: 0x50,
+
+ serverSocket: null,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIInputStreamCallback",
+ "nsIServerSocketListener",
+ ]),
+
+ /**
+ * Start listening on an OS-selected port. The port number can be found at
+ * LDAPServer.port.
+ */
+ open() {
+ this.serverSocket = Cc[
+ "@mozilla.org/network/server-socket;1"
+ ].createInstance(Ci.nsIServerSocket);
+ this.serverSocket.init(-1, true, 1);
+ console.log(`socket open on port ${this.serverSocket.port}`);
+
+ this.serverSocket.asyncListen(this);
+ },
+ /**
+ * Stop listening for new connections and close any that are open.
+ */
+ close() {
+ this.serverSocket.close();
+ },
+ /**
+ * The port this server is listening on.
+ */
+ get port() {
+ return this.serverSocket.port;
+ },
+
+ /**
+ * Retrieves any data sent to the server since connection or the previous
+ * call to read(). This should be called every time the application is
+ * expected to send data.
+ *
+ * @returns {Promise} Resolves when data is received by the server, with the
+ * data as a byte array.
+ */
+ async read(expectedOperation) {
+ let data;
+ if (this._data) {
+ data = this._data;
+ delete this._data;
+ } else {
+ data = await new Promise(resolve => {
+ this._inputStreamReadyResolve = resolve;
+ });
+ }
+
+ // Simplified parsing to get the message ID and operation code.
+
+ let index = 4;
+ // The value at [1] may be more than one byte. If it is, skip more bytes.
+ if (data[1] & 0x80) {
+ index += data[1] & 0x7f;
+ }
+
+ // Assumes the ID is not greater than 127.
+ this._lastMessageID = data[index];
+
+ if (expectedOperation) {
+ let actualOperation = data[index + 1];
+
+ // Unbind and abandon requests can happen at any point, when an
+ // nsLDAPConnection is destroyed. This is unpredictable, and irrelevant
+ // for testing. Ignore.
+ if (
+ actualOperation == LDAPServer.UnbindRequest ||
+ actualOperation == LDAPServer.AbandonRequest
+ ) {
+ if (PRINT_DEBUG) {
+ console.log("Ignoring unbind or abandon request");
+ }
+ return this.read(expectedOperation);
+ }
+
+ Assert.equal(
+ actualOperation.toString(16),
+ expectedOperation.toString(16),
+ "LDAP Operation type"
+ );
+ }
+
+ return data;
+ },
+ /**
+ * Sends raw data to the application. Generally this shouldn't be used
+ * directly but it may be useful for testing.
+ *
+ * @param {byte[]} data - The data to write.
+ */
+ write(data) {
+ if (PRINT_DEBUG) {
+ console.log(
+ ">>> " + data.map(b => b.toString(16).padStart(2, 0)).join(" ")
+ );
+ }
+ this._outputStream.writeByteArray(data);
+ },
+ /**
+ * Sends a simple BindResponse to the application.
+ * See section 4.2.2 of the RFC.
+ */
+ writeBindResponse() {
+ let message = new Sequence(0x30, new IntegerValue(this._lastMessageID));
+ let person = new Sequence(
+ 0x61,
+ new EnumeratedValue(0),
+ new StringValue(""),
+ new StringValue("")
+ );
+ message.children.push(person);
+ this.write(message.getBytes());
+ },
+ /**
+ * Sends a SearchResultEntry to the application.
+ * See section 4.5.2 of the RFC.
+ *
+ * @param {object} entry
+ * @param {string} entry.dn - The LDAP DN of the person.
+ * @param {string} entry.attributes - A key/value or key/array-of-values
+ * object representing the person.
+ */
+ writeSearchResultEntry({ dn, attributes }) {
+ let message = new Sequence(0x30, new IntegerValue(this._lastMessageID));
+
+ let person = new Sequence(0x64, new StringValue(dn));
+ message.children.push(person);
+
+ let attributeSequence = new Sequence(0x30);
+ person.children.push(attributeSequence);
+
+ for (let [key, value] of Object.entries(attributes)) {
+ let seq = new Sequence(0x30, new StringValue(key), new Sequence(0x31));
+ if (typeof value == "string") {
+ value = [value];
+ }
+ for (let v of value) {
+ seq.children[1].children.push(new StringValue(v));
+ }
+ attributeSequence.children.push(seq);
+ }
+
+ this.write(message.getBytes());
+ },
+ /**
+ * Sends a SearchResultDone to the application.
+ * See RFC 4511 section 4.5.2.
+ */
+ writeSearchResultDone() {
+ let message = new Sequence(0x30, new IntegerValue(this._lastMessageID));
+ let person = new Sequence(
+ 0x65,
+ new EnumeratedValue(0),
+ new StringValue(""),
+ new StringValue("")
+ );
+ message.children.push(person);
+ this.write(message.getBytes());
+ },
+
+ /**
+ * nsIServerSocketListener.onSocketAccepted
+ */
+ onSocketAccepted(socket, transport) {
+ let inputStream = transport
+ .openInputStream(0, 8192, 1024)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+
+ let outputStream = transport.openOutputStream(0, 0, 0);
+ this._outputStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ this._outputStream.setOutputStream(outputStream);
+
+ if (this._socketConnectedResolve) {
+ this._socketConnectedResolve();
+ delete this._socketConnectedResolve;
+ }
+ inputStream.asyncWait(this, 0, 0, Services.tm.mainThread);
+ },
+ /**
+ * nsIServerSocketListener.onStopListening
+ */
+ onStopListening(socket, status) {
+ console.log(`socket closed with status ${status.toString(16)}`);
+ },
+
+ /**
+ * nsIInputStreamCallback.onInputStreamReady
+ */
+ onInputStreamReady(stream) {
+ let available;
+ try {
+ available = stream.available();
+ } catch (ex) {
+ if (
+ [Cr.NS_BASE_STREAM_CLOSED, Cr.NS_ERROR_NET_RESET].includes(ex.result)
+ ) {
+ return;
+ }
+ throw ex;
+ }
+
+ let binaryInputStream = Cc[
+ "@mozilla.org/binaryinputstream;1"
+ ].createInstance(Ci.nsIBinaryInputStream);
+ binaryInputStream.setInputStream(stream);
+ let data = binaryInputStream.readByteArray(available);
+ if (PRINT_DEBUG) {
+ console.log(
+ "<<< " + data.map(b => b.toString(16).padStart(2, 0)).join(" ")
+ );
+ }
+
+ if (this._inputStreamReadyResolve) {
+ this._inputStreamReadyResolve(data);
+ delete this._inputStreamReadyResolve;
+ } else {
+ this._data = data;
+ }
+
+ stream.asyncWait(this, 0, 0, Services.tm.mainThread);
+ },
+};
+
+/**
+ * Helper classes to convert primitives to LDAP byte sequences.
+ */
+
+class Sequence {
+ constructor(number, ...children) {
+ this.number = number;
+ this.children = children;
+ }
+ getBytes() {
+ let bytes = [];
+ for (let c of this.children) {
+ bytes = bytes.concat(c.getBytes());
+ }
+ return [this.number].concat(getLengthBytes(bytes.length), bytes);
+ }
+}
+class IntegerValue {
+ constructor(int) {
+ this.int = int;
+ this.number = 0x02;
+ }
+ getBytes() {
+ let temp = this.int;
+ let bytes = [];
+
+ while (temp >= 128) {
+ bytes.unshift(temp & 255);
+ temp >>= 8;
+ }
+ bytes.unshift(temp);
+ return [this.number].concat(getLengthBytes(bytes.length), bytes);
+ }
+}
+class StringValue {
+ constructor(str) {
+ this.str = str;
+ }
+ getBytes() {
+ return [0x04].concat(
+ getLengthBytes(this.str.length),
+ Array.from(this.str, c => c.charCodeAt(0))
+ );
+ }
+}
+class EnumeratedValue extends IntegerValue {
+ constructor(int) {
+ super(int);
+ this.number = 0x0a;
+ }
+}
+
+function getLengthBytes(int) {
+ if (int < 128) {
+ return [int];
+ }
+
+ let temp = int;
+ let bytes = [];
+
+ while (temp >= 128) {
+ bytes.unshift(temp & 255);
+ temp >>= 8;
+ }
+ bytes.unshift(temp);
+ bytes.unshift(0x80 | bytes.length);
+ return bytes;
+}
diff --git a/comm/mailnews/addrbook/test/moz.build b/comm/mailnews/addrbook/test/moz.build
new file mode 100644
index 0000000000..c513212222
--- /dev/null
+++ b/comm/mailnews/addrbook/test/moz.build
@@ -0,0 +1,14 @@
+# vim: set filetype=python:
+# 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/.
+
+TESTING_JS_MODULES += [
+ "CardDAVServer.jsm",
+ "LDAPServer.jsm",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "unit/xpcshell.ini",
+ "unit/xpcshell_cardDAV.ini",
+]
diff --git a/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js b/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js
new file mode 100644
index 0000000000..4d810d8fc0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js
@@ -0,0 +1,7 @@
+/* globals user_pref */
+user_pref("ldap_2.servers.extension.description", "extension");
+user_pref("ldap_2.servers.extension.filename", "ldap1.mab");
+user_pref(
+ "ldap_2.servers.extension.uri",
+ "ldap://test.invalid:389/o=invalid??sub"
+);
diff --git a/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql b/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql
new file mode 100644
index 0000000000..68a226c325
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql
@@ -0,0 +1,95 @@
+-- Address book data for use in various tests.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 1),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 2),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 3),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 4);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'LastName', 'Email'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'DisplayName', 'Empty Email'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'FirstName', 'Empty'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'AllowRemoteContent', '0'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'PopularityIndex', '0'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'PreferMailFormat', '0'),
+ ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'LastModifiedDate', '0'),
+
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LastName', 'LastName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom4', 'Custom41'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LastModifiedDate', '1237281794'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WebPage2', 'http://WebPage11'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'NickName', 'NickName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'DisplayName', 'DisplayName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkZipCode', 'WorkZipCode1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', '_AimScreenName', 'ScreenName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkAddress', 'WorkAddress1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeCountry', 'HomeCountry1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkPhone', 'WorkPhone1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PrimaryEmail', 'PrimaryEmail1@test.invalid'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeAddress', 'HomeAddress11'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LowercasePrimaryEmail', 'primaryemail1@test.invalid'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkCity', 'WorkCity1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'SecondEmail', 'SecondEmail1Ð@test.invalid'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeZipCode', 'HomeZipCode1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom3', 'Custom31'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'FaxNumber', 'FaxNumber1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom1', 'Custom11'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomePhone', 'HomePhone1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'FirstName', 'FirstName1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeCity', 'HomeCity1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PagerNumber', 'PagerNumber1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'CellularNumber', 'CellularNumber1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkAddress2', 'WorkAddress21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkState', 'WorkState1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeAddress2', 'HomeAddress21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WebPage1', 'http://WebPage21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Notes', 'Notes1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom2', 'Custom21'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Department', 'Department1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkCountry', 'WorkCountry1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeState', 'HomeState1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'JobTitle', 'JobTitle1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Company', 'Organization1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PopularityIndex', '0'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'AllowRemoteContent', '1'),
+ ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PreferMailFormat', '0'),
+
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LastModifiedDate', '1245128765'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'NickName', 'johnd'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'DisplayName', 'John Doe'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LastName', 'Doe'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PrimaryEmail', 'john.doe@mailinator.invalid'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'FirstName', 'John'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LowercasePrimaryEmail', 'john.doe@mailinator.invalid'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'AllowRemoteContent', '0'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PopularityIndex', '0'),
+ ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PreferMailFormat', '0'),
+
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'NickName', 'janed'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'DisplayName', 'Jane Doe'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LastName', 'Doe'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PrimaryEmail', 'jane.doe@mailinator.invalid'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'FirstName', 'Jane'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LowercasePrimaryEmail', 'jane.doe@mailinator.invalid'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LastModifiedDate', '0'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'AllowRemoteContent', '0'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PopularityIndex', '0'),
+ ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PreferMailFormat', '0'),
+
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'DisplayName', 'A vCard!'),
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'PrimaryEmail', 'first@something.invalid'),
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'SecondEmail', 'second@something.invalid'),
+ ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', '_vCard', 'BEGIN:VCARD
+FN:A vCard!
+EMAIL:first@something.invalid
+EMAIL:second@something.invalid
+EMAIL:third@something.invalid
+EMAIL:fourth@something.invalid
+END:VCARD');
diff --git a/comm/mailnews/addrbook/test/unit/data/collect.sql b/comm/mailnews/addrbook/test/unit/data/collect.sql
new file mode 100644
index 0000000000..dba17a7392
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/collect.sql
@@ -0,0 +1,21 @@
+-- Collection address book for use in test_collection_2.js.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 1);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'DisplayName', 'Other Book'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'LastName', 'Book'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'PrimaryEmail', 'other@book.invalid'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'FirstName', 'Other'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'LowercasePrimaryEmail', 'other@book.invalid'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'LastModifiedDate', '0'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'AllowRemoteContent', '0'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'PopularityIndex', '0'),
+ ('28fd662c-1662-4b02-8950-12dd131a1116', 'PreferMailFormat', '0');
diff --git a/comm/mailnews/addrbook/test/unit/data/export.csv b/comm/mailnews/addrbook/test/unit/data/export.csv
new file mode 100644
index 0000000000..92da47c7be
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.csv
@@ -0,0 +1,4 @@
+First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Screen Name,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes
+contact,one,contact number one,,contact1@invalid,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
+contact,two,contact number two,,contact2@invalid,,,,,,,,,,,,,,,,,,,,"""worker""",,,,,,,,"custom, 1","custom 2","custom 3","custom
+4",here's some unicode text…
diff --git a/comm/mailnews/addrbook/test/unit/data/export.ldif b/comm/mailnews/addrbook/test/unit/data/export.ldif
new file mode 100644
index 0000000000..669b63f6a4
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.ldif
@@ -0,0 +1,36 @@
+dn: cn=new list
+objectclass: top
+objectclass: groupOfNames
+cn: new list
+member: cn=contact number one,mail=contact1@invalid
+
+dn: cn=contact number one,mail=contact1@invalid
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+objectclass: mozillaAbPersonAlpha
+givenName: contact
+sn: one
+cn: contact number one
+mail: contact1@invalid
+modifytimestamp: 12345
+
+dn: cn=contact number two,mail=contact2@invalid
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+objectclass: mozillaAbPersonAlpha
+givenName: contact
+sn: two
+cn: contact number two
+mail: contact2@invalid
+modifytimestamp: 12345
+title: "worker"
+mozillaCustom1: custom, 1
+mozillaCustom2: custom 2
+mozillaCustom3:: Y3VzdG9tDTM=
+mozillaCustom4:: Y3VzdG9tCjQ=
+description:: aGVyZSdzIHNvbWUgdW5pY29kZSB0ZXh04oCm
+
diff --git a/comm/mailnews/addrbook/test/unit/data/export.txt b/comm/mailnews/addrbook/test/unit/data/export.txt
new file mode 100644
index 0000000000..82f9c468ae
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.txt
@@ -0,0 +1,4 @@
+First Name Last Name Display Name Nickname Primary Email Secondary Email Screen Name Work Phone Home Phone Fax Number Pager Number Mobile Number Home Address Home Address 2 Home City Home State Home ZipCode Home Country Work Address Work Address 2 Work City Work State Work ZipCode Work Country Job Title Department Organization Web Page 1 Web Page 2 Birth Year Birth Month Birth Day Custom 1 Custom 2 Custom 3 Custom 4 Notes
+contact one contact number one contact1@invalid
+contact two contact number two contact2@invalid """worker""" "custom, 1" "custom 2" "custom 3" "custom
+4" here's some unicode text…
diff --git a/comm/mailnews/addrbook/test/unit/data/export.vcf b/comm/mailnews/addrbook/test/unit/data/export.vcf
new file mode 100644
index 0000000000..91cc8b7016
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/export.vcf
@@ -0,0 +1,20 @@
+BEGIN:VCARD
+VERSION:4.0
+EMAIL;PREF=1:contact1@invalid
+FN:contact number one
+N:one;contact;;;
+UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+END:VCARD
+BEGIN:VCARD
+VERSION:4.0
+EMAIL;PREF=1:contact2@invalid
+FN:contact number two
+NOTE:here's some unicode text…
+TITLE:"worker"
+N:two;contact;;;
+X-CUSTOM1;VALUE=TEXT:custom\, 1
+X-CUSTOM2;VALUE=TEXT:custom 2
+X-CUSTOM3;VALUE=TEXT:custom 3
+X-CUSTOM4;VALUE=TEXT:custom\n4
+UID:yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json b/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json
new file mode 100644
index 0000000000..c239820d51
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json
@@ -0,0 +1,104 @@
+{
+ "eurus": {
+ "dn": "uid=eurus,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Eurus Holmes",
+ "givenName": "Eurus",
+ "mail": "eurus@bakerstreet.invalid",
+ "sn": "Holmes"
+ }
+ },
+ "irene": {
+ "dn": "uid=irene,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Irene Adler",
+ "givenName": "irene",
+ "mail": "irene@bakerstreet.invalid",
+ "sn": "Adler"
+ }
+ },
+ "john": {
+ "dn": "uid=john,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "John Watson",
+ "givenName": "John",
+ "mail": "john@bakerstreet.invalid",
+ "sn": "Watson"
+ }
+ },
+ "lestrade": {
+ "dn": "uid=lestrade,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Greg Lestrade",
+ "givenName": "Greg",
+ "mail": "lestrade@bakerstreet.invalid",
+ "o": "New Scotland Yard",
+ "sn": "Lestrade"
+ }
+ },
+ "mary": {
+ "dn": "uid=mary,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Mary Watson",
+ "givenName": "Mary",
+ "mail": "mary@bakerstreet.invalid",
+ "sn": "Watson"
+ }
+ },
+ "molly": {
+ "dn": "uid=molly,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Molly Hooper",
+ "givenName": "Molly",
+ "mail": "molly@bakerstreet.invalid",
+ "o": "St. Bartholomew's Hospital",
+ "sn": "Hooper"
+ }
+ },
+ "moriarty": {
+ "dn": "uid=moriarty,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Jim Moriarty",
+ "givenName": "Jim",
+ "mail": "moriarty@bakerstreet.invalid",
+ "sn": "Moriarty"
+ }
+ },
+ "mrs_hudson": {
+ "dn": "uid=mrs_hudson,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Mrs Hudson",
+ "givenName": "Martha",
+ "mail": "mrs_hudson@bakerstreet.invalid",
+ "sn": "Hudson"
+ }
+ },
+ "mycroft": {
+ "dn": "uid=mycroft,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Mycroft Holmes",
+ "givenName": "Mycroft",
+ "mail": "mycroft@bakerstreet.invalid",
+ "sn": "Holmes"
+ }
+ },
+ "sherlock": {
+ "dn": "uid=sherlock,dc=bakerstreet,dc=invalid",
+ "attributes": {
+ "objectClass": "person",
+ "cn": "Sherlock Holmes",
+ "givenName": "Sherlock",
+ "mail": "sherlock@bakerstreet.invalid",
+ "sn": "Holmes"
+ }
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat b/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat
new file mode 100644
index 0000000000..7621d1e76d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat
@@ -0,0 +1,17 @@
+version="9"
+logging="no"
+name="From is in book 7"
+enabled="yes"
+type="17"
+action="Mark flagged"
+condition="AND (from,is in ab,moz-abmdbdirectory://abook-7.mab) AND (subject,contains,nothing)"
+name="From is not in book 8"
+enabled="yes"
+type="17"
+action="Mark flagged"
+condition="AND (from,isn't in ab,moz-abmdbdirectory://abook-8.na2.mab)"
+name="Not related"
+enabled="yes"
+type="17"
+action="Mark read"
+condition="AND (subject,contains,unrelated)"
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf
new file mode 100644
index 0000000000..f877f281ec
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf
@@ -0,0 +1,101 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:Binary JPEG
+NOTE:This v3.0 card has a JPEG photo as binary data and binary valuetype.
+PHOTO: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
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf
new file mode 100644
index 0000000000..720a5a009c
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:Binary PNG
+NOTE:This v3.0 card has a PNG photo as binary data and binary valuetype.
+PHOTO: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
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf
new file mode 100644
index 0000000000..c856944ae6
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf
@@ -0,0 +1,102 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI JPEG, version 3.0
+NOTE:This v3.0 card has a JPEG photo as a URI and binary valuetype.
+PHOTO:data:image/jpeg;base64,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
+ QAvGHlaJiBABAH0//9k=
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf
new file mode 100644
index 0000000000..bea653f3d5
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI PNG, version 3.0
+NOTE:This v3.0 card has a PNG photo as a URI and binary valuetype.
+PHOTO:data:image/png;base64,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
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf
new file mode 100644
index 0000000000..be583a9bfd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf
@@ -0,0 +1,102 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI JPEG, version 3.0 with correct value type
+NOTE:This v3.0 card has a JPEG photo as a URI and URI valuetype.
+PHOTO;VALUE=URI:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYE
+ 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
+ QJz0Xq7X9hQAvGHlaJiBABAH0//9k=
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf
new file mode 100644
index 0000000000..a56eb3ed97
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:URI PNG, version 3.0 with correct value type
+NOTE:This v3.0 card has a PNG photo as a URI and URI valuetype.
+PHOTO;VALUE=URI:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAA
+ 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
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf
new file mode 100644
index 0000000000..f93a68969b
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf
@@ -0,0 +1,102 @@
+BEGIN:VCARD
+VERSION:4.0
+FN:URI JPEG, version 4.0
+NOTE:This v4.0 card has a JPEG photo as a URI and URI valuetype.
+PHOTO:data:image/jpeg;base64,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
+ QAvGHlaJiBABAH0//9k=
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf b/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf
new file mode 100644
index 0000000000..058d5fbbf2
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf
@@ -0,0 +1,204 @@
+BEGIN:VCARD
+VERSION:4.0
+FN:URI PNG, version 4.0
+NOTE:This v4.0 card has a PNG photo as a URI and URI valuetype.
+PHOTO:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAA
+ q0klEQVR42u2dB1RU57r3XXd965R859yP9d17Ts45SRTpvTOFOgiKDcUWY4kSe4kGu7EgNoogg
+ p0+gIoGFRRFLMBQREGRURBrDDG5OelO2knul3vO8z3vu/ee2XtmDwxNR2Wv9V8qIs7M7/8+7X3
+ 3zIAB/Vf/9bxfQwseWYTlfRgVmvtQOSTjzvWQ9Lu/BCdebg6KOT+o/9V5ga/wIx9FDjv0qAThA
+ 8KHIQda/1ux7vQ/gt8rHN3/6rzA1/Cj7VEIvx3hQ1g+wlc+hJDNFyB4QU5L8Jx0C/I9gfvuWAZ
+ urd4duKmivP8Ve3HAK1DtCB8I/KEIf8g+NSjeOwwIXk3gB6feGB20q/lhQFrLPwPXnvkZv+Yh9
+ rO8t95QeMU2Kfpf1ecDvAWqBAUC+Duv4KrPJvA1iqQrbwWnNbcH7b8DgXvbIGBrDfjHN9TJk27
+ WyRJufC1NvAm+8Wrw2d4MCB+8NjeB56br4LHxGritawTXtVfBZfUVcF5ZD07LL6scoutU9ktrU
+ +3erYm1XVQdabNA5dFP4tmteg2FX4jwDyP8Agz7uPIpfBTC/z744F3g4Pvvvg1+qa0gT2kBWdI
+ tIPAlxuCvR/jvN4DLmqsI/wrCrwfH6MuA8AHhA8IHhA/W86pg8JxKjeWsCtXgqEvRVjMu9Bvia
+ eR6Ap6B366Dn9EKITFlEJJUD4qMe8CHH8CHn4zwdyD8BDX4isB358NfhfBXIPxll8FhaR3Cr0X
+ 4NQi/msK3mlMJCB8QPiB8sH77PNhMO9duO/Vsqt1bZ/rN0AfwlRz84RT+Rwgfiz6u4s++DyGZC
+ D8d4R/QwfcXgx/XDD7bEP4WIXw3Mfjv1YH9Ej58FcLH1T+rkoE/8yIHHxA+IHywn3waHCeWfIq
+ /T0JzBPbT6034ZPUbwH+A8O8j/HsI/y4E7kP4exj4fkbgexP4sQg/BuFvYOC7GoO/GOEvRPjzd
+ fAtEf5gDv50Ar+MGoAYwQq/ZvlOhVZolP+HOo6/j0JZ9BPtwhWW3lopgM8WfbTd48FXaOHfQfi
+ Y99Mw9O/C1b/TBPjrGrRFnz58Oz78uQh/diUDFeET0NbTyxnomAb40DvQ9yglqr/jMHa5j9pv4
+ T5yX6z/6vJvxeAPZXv9ITkIP4uBHywKnyn6JFzFz8L3MgKfVPyOyxF+tA6+rTH4Mxj41m+X49c
+ umQpfIKuZF+6SqNBPnL3cItItULFuo9M1koXFYBJ8XtEnBl+03Yu5LgrfiYPPVfwI30YPPgn9X
+ Pgnq74LK9+oMIX88FIbwXVMlgUqFqVBgdfMYzCs4KEOfqEOfpgR+Np2rzP4/HZvLRZ9q6+Kw+e
+ 1e6Ta7ylkk4wwrexnpwkn575U8F0ic6OcI3PbnSOVwCgXQg+2CuGz7V5Y3kMINQLfoN3roNc3a
+ Pe0vX6dEP7cpwefL8eJxU88Rh4a8UKDdx6X5+E0Ll/lND4fnMYXaCXfWAPCXv8jHfzcB7TdU2Q
+ awjfW7onBdzOp11c9dfD6QhPUvpBdA4KOdZpwCBwnHGY08QiV+5xT0HGv/6B7vX6sib0+V/ET+
+ LMqnrkBiGynnP2n91DlkhcCPMK2RNBqAtthUiHqKKtjVIo9twRFn1ivb9juYa+/y7Re32N9A/i
+ Qwk900GNY7ZuN0Iw4Y/j8uY4GCD0KoWso7DePgf2bH+DErEgrz+hL4r2+UgjfWLsnJe2eHnyu3
+ fPY2Ai+GAFk+Ksr6fVXMvAJeLLa6WDHnICz0HWqpBr8zqV/OUwqefe5g4+rXMmHbjf5OI5MT6B
+ OMppyEsJy7nfe7pnS65Oij4XvvglXfuw1kOKf5dj6ua1h2j2HZXVgvUD1XEDXwp+tE46aC58X8
+ BYIXmX/JlnlLHSEbTulGFWCYY3oFPhsqDeAb6zdC9hrvNf3RvieuPrdtzaB6xZc/firDItAv1g
+ Cn2n3HBH+4DmVzx10KjKGZmU39cx1M4d/zBJXvFoHnoN+Cvvd06xKwXnRRWG7x8IP7ajXTxXC9
+ 8HQ7xHfDG5xN8BlWxM4b70OnvEIHk3hhwbwfL9Ru7VrZQ7wuwl98FydrDB1OUw+/ZlZ1gW46j0
+ w1Gu04KfywE8vxRHqGdRZsJ11nq54g14/t/NeX4pFn0/STfBIbAbXhBvgHNcEjtuvg8M2zPk7m
+ iEg/ib4bcN6YON1bcVP2jtzg24AvBPoVPNZ4fPBTahvzcoEuOoR/nENye2CFc9Ct367DDdRyOb
+ JOQje3ybS7vF6/XTxXt97501wS1aDC4J2TrwBDgnXwT7uGthuawT3JDX4J9yCgO1qkMY2aTd4y
+ JTvqbd2fQSdyHohI2qCKWe/MQsTEPh2byF8kudx1RuAn0HAl+Mc/Tz4Yqgm8Dve2r0j2Nr1Tbk
+ FrjvV4Jx8Axx3NIFD4nWwS7gGNnGNYLUd+/uduPJ33MKjX7j6tzbTvM+1e6S/Nyvoc7oLvQasF
+ 7FaTFRLW1eXyKL7ZgD/hIYWeHTVs6FeC/48gr+Ae+MXcepWI2z3Oun1Zdjru+9SgyOCd0hqArs
+ d18EGwVvFN8DguKswaDuu8pQbEJDcAgGJt8AvTg1eOPTh9/qkx7fDx2I3veyFgG6DU0ubJXWM8
+ PfkcTwzEyB8C4TfLoR/lg31zIon4MkJGrJ1SsK6Qa+fI97r+6a2gCPCtUtuApsdCD2xEQYnIPS
+ 4KzBwez0M3HYZnHc2QUBKK7P6E26CdPMNba/P39cX29p9VtC1wE2CXmsIHUfXtu9dBttoonr6c
+ 8jj8g7Pq3kW8NVa+KTIe/sss+pn6lY93TPHF4tswnS6tcvCd8NVb5OM0JMQemIDDGLBW26/DJb
+ bsJ3bUgvOOxohAFtCsvr9cfXLMfeTYY+jCVu73JYu2dcnhzvIn80JOgXeAXTbZfVgh52N3Yor+
+ Pe12sfpPUyZ/NQMgEMdpRB+mWDV0/1z8sDwRRk0r5K2cZ31+v57b4NDShMM3oEhPvEqhnp0eNx
+ lsELoVlsx523BFyhWBY74tYDU2xCwE2uEHS3Y9t0ETxL6jW3t8uFzhznZEz1YSGG7WopDKhK9z
+ rHRqpvQ5/YS9KUdQ7fD1tZ+1VUquxVXdQdNMN16hysn9f3qn3w8min42LCvD59d9ZZzEf6CKhi
+ 0qErX7nXQ69vjirdOwLYtHou37fgibMNZ/VasdjerwDYWhyAxleC0pRoCcCAUsAtnA8m4H5DYA
+ pItzZ0d49Y7yVvOO8xZipPK07gnUYIbVCdxZ/IEPdxJjdBj6NV9At1+dQPYr2kAh7WN4ICzDn6
+ 0wsf+T59huYP6EH6RB231aLXPhf1yXsjHBzKbWfWDFiL8d3HFbrhquLWrB989+TrYxuHW7PYas
+ NtWDXZbVGC/uQrsN+EINOYSOGy4CM6bKnAecBMCCXy6+rE9xNbPFV+MLh7jFpzkpfAnFlP4zpF
+ F4DL2A3CNOAqO+HVrUsM8LejLOOhXhNBX60O/Bg7rroHj+uv42l6nj4NvArcxRzV9XPQxrR4t+
+ DDn61Y+G/LnM6t+0BIVDIyuBoeYhg6PccvTWnCQg9+3FadcW6rAMbYCnDZdAqeNF8FpwwVwXn8
+ eXNaVgxxNEpjaRkO/XxKz+r1jmrpxjLuMB/+UDv644wz8MUfBbXQhuI86DB4jDoHzhBNg/c7Fr
+ kNf3EPoa4xDd8BBlwM+d4dNTWAl0upKQ9LP9EXoTyV532bqadrqkWrfAD4J+YtVMGhpNQxchlp
+ ZAw6bG420e0yv7xGPmzWbK3CFXwLnmIvgsvECuKwvx8LxHLitLcO+/izI4uohcDcWiSm48pNug
+ xzhy+Ju0tDf1WPcdlP04Z/kwT8mgO85PB+8huWB99BccJ5UjPVEZR9AFw/tWugI3HGjELo9Drv
+ s8cCL/ZYbYLXI0ACkHpCGZAzvzdWvYEI/r+gzAn/gewh+eQ0MXIXhfP5ZkEed7PAYtxuGdreN5
+ 8FtQzm4T00H9yFbdBqeAL4rSiCQ5P1UJvST1S+Lv4X7/Ne6dIxbB78Ut6ZZ+BNY+JF8+EdY+AX
+ gFc7A9wnLAd8hWaTSBruoC92CTvN5Z9DpKudBj9FBt9twDazJ80XodrjXYYcbYLbbbxjtXJwmF
+ f/am1U/7fdp6OflfVrwzWbDPg++3YJzIBmVAwp5CoTgmT9jx7hlSc0I8hxu3JSBx4TdQvisfGc
+ qGfi4+v2w8JNj5S/ZpqYtn6nHuPHgJVPxa+GX8OAXMfAjWPgj+fCVLPxskIRkgjQ4HVdWJjiTK
+ Kjt0WuF7VpvQN9kuNIJdPK8rPFEsy2ef7DFjTCbhGYD8INnM7KaUwE+I/OaeqPwi+VWPw392rz
+ PVPu04FuEYR/hW0arwOPNQggM3gNBAWkwaWwGKPMbjB7jluJUz3tNKXjNUmqBLwwfC3EjfWHfD
+ F9YM3sSeE06IIAvw8LPDU/2dtjrR13ktXtl2nbPHuGToo+Bf4IH/6hx+KEsfEUGyIIOgizwAMj
+ 994ELmqDXoccaQudWOoHOgbbGbW+bHWpsj68zz5cDP4cBbzUXvwe52M65CPJhmd1PBbjDZ0Hm/
+ Ezhx7V8vNBPWj1S8C3Flgu3eH0jlOA/ZD81wNYt5+Drr7+DX375xegxbmlsNUhXFIPHqB0U/lh
+ fb3jlf/0b+L72e8if9yq0K18D76lZCB/zPlb9soQW8N2qFm/39Hv9t4W9vn67x6/43UYT+IfBc
+ wSBn09DPYHvg/B9Eb4E4Us5+AH7wM9vL/jLd4MbRkRBu9Zd6JsZ2YlA51a6TaLOAIPxuVvh/og
+ VHnzhVjuBzoG3nl+JrwtqYSV4Tvrgxx7k/iIlWf02gtXPhn4u7y8h8C+AdGQ2+IUdhGERWXD69
+ C0KntO4zLuix7hlMVXgF11E4YeFzQGNRgMeHh6A/zWEuv0Ovj71GkiWlYGc5P1EjBhxLTjxazC
+ Ab9Vhr3+GB79YFL77SFL0FYBnOCn6lOAdlsvCz2LgY+hn4O9n4e+BAFkaBEpSwX366e5D39I5d
+ LLSrXG30xp3QrVhnigBO4C19Sz0CgrdeoEOvO0i1OJKfK0qwS88c2+3DneQY1yC1c8Vflzox7w
+ /+N0KkIzOAXl4BgwdmwOtbZ8J4BOtKX4kepJXvv4S+C85Rg0wUj5KC59I5vhbuFXojtV+MwMfV
+ 78PDn30j3ET+IM5+DNN6PW5ij+CbfdGshU/B38oC3+ICHx/Ifwg3xQI9tkJrvMudA06C9wU6HS
+ lp9wEq103Bfl98Pv1YI1zFv5q1wdvtwTnKEsrwT3q1K/dqfyVhrmft/px0GP5bhV44RFv2YgsC
+ BunFIVPlFP/megxbtn7mKMWHwWP8DiYMHQWBL7xCoX/x9//G9Tu+TOsSE7Qwpdsv0XbPv4xbn6
+ 7Z6Xf7nXY6xtv93xCc4Twg/Th74ZAKYG/i8IP9k6GIEkKOOHZQx30JuPQt5sAHQ+5aKHj8GtwG
+ iq5WZDf+eApdBHw9u9V4nicqAqCQvfndOVcnyU5yKmr/PVW/3xmyuc67ThIsdoPGZcLrXfE4RM
+ 1PHoieoxbsrYCZAsKwSsSX9DQVXBu/utwa+drcCvXETalxfBWvhqcSK7Vb/fYk702BDivwrcnO
+ Z/f7on2+keMw2crflL0MfD3CeAHIvwgFr7CMwlCPBLBPzRdr3LXgx5nCN3ayEq3XFIDgzfhZtj
+ uW2CJx+Yt96ISm8TBE+jGwC/DwRrR8irwmV70r64Uf7HkaBcd+Yqtfiz8bOafBwkWfcQA2xNzs
+ OD72qgBrqIBxI5x+6yvBcm8I+Az6xAWe7jiFp2AiNgiWvHLsOiTYL/vzvb7+u2eLeZ50r+Tin3
+ k9CKIxnHx/pxrsD+7kepAVgMk7qmHDYnV8M7qCzDx3bPgN6nISLun6/VF4fsx8AMIfAmBn8LA9
+ 2LgD3FPwJolHnzePmkIPd406NqVjtAHv1dDV/ugXc0waH8LleWmBtH8TqCLgkfojiuq8HwEamU
+ VHpGrgjBpUrqp4V/DFH/cyLdcW/lzq5+EfmKArTvPQXp6Opw6dcq4AT58InKMG+/g2XgFfOYcA
+ e9Zh8E/tlbb7slw0ueJq4kc7uD3+qTds5mFE0MM52T1rt9RAy13Pjf6/xpT/fVPIK+ohZpj8rw
+ SYa/PtntyzPsEPi36KPw0Efg7tPBDXeNgiGcirv5rDPSErkHnVvqgfWiAZbW0sCN5fuDBFhiY3
+ grWy2uMhnlR8Ct14J1X4+mhNVXgH5nziynhP0oX/pnij/T9dIuXzf2OUafBd2weTF1yCn799Vc
+ Kn5igublZ9AU/c+Nzo3ftes06CtLoM3R3j/T55DZueo5fb2vXFqt9hymldPUuxd3Bjx5/02XwH
+ elK48c0aqzA8fPYiQW01+faPS18UvR572TgezLwh7Dww5y3wVCnrSCPyGXyeSfQLfWg05V+oIU
+ Ct4qu0a72N4gB9qg7zO+O+uBZ6Bx4l7VVOFqvAt/ZJ2CYx/bozqr/EvHwr6v8PScWUgO03f+SG
+ uDJkyeQm5tLTfDZZ4a1wMG6+xCULH7XrtcShB9H7t9v4r1DlxC+3awLdGgz4d0yqG/6pFfBG9P
+ XX38PDVcfQcaBOlizvASipuTrwU/UwXfZTuEPc9wC4Q6bsQ6o7zL0gemojFZ4A8UP8wPTsPjbd
+ KXD/O6kD36NELzruiq8NV4F7lhzDZUkPuzoVi4LciePrvfnhX9S/GHfbzenDHwiC2DTrloKn1N
+ bWxs1QFFRkcGLmVJzG9aVtRjAl+Ikyw+Pdwtu3Izmbe1iq+c4+RTI8fHknrj9VMB3praWT6Dq/
+ G3I3lMFce+XQPSMPIiU7qDwhyH8cPtY8BuT3Sl0mt9TmylwqsxWeD2rFQbuvSlY7dYb603K71r
+ wCN3lfSF4tw0IfyMqRgWKYfthmOf2QUYMUBilX/0Lwj8Wfy7TTlADtD34UmAAonPnmHqgoaFB+
+ 4LVPPw7lDxso0pUtcCbmc0wKVMNs4vaYO7ZR6DY3Wpw164d5n37qPOY64/D4q018NEn35oF/M7
+ 0zVcauF57H3UPfHFFG1vpBPjAlGaa1wn017Nvw+s5t+G13NtY7V/Trfalpud3Lfh1RCoD8B6bU
+ HiiKjAyG4b6JhYby/8l9rzhD7Plqwv/luhM7/GHYEp0qQF8op9++skgFaRU39YagCjz5l3YXv8
+ All74EN46+RGE45RQcNcutjmO2L6FziuDyzf+67kAL6aN1Z8YQCer/A0KHA2Aq5+E+YG7bsBrS
+ oSf1wZ/y28D27W1gvzuYGJ+p+DXGwfvuZmR34xCCA1M+Yex/A9c/meGP+WC3t92bjk1wKGSVlE
+ DED18+FCQCvJu6gxwuO0OpFy7D2urHsI7pY9gwvF2Kh+8j88Bq16HqWfAd0YppB1ueW7Bc2r+7
+ DsD6GSVcyt9EFnpuKCsMMT/raAN/nqoDV7Lae12fifQ3TYaAY8nrLyI8NCNfM4HoAjdC8N8EgL
+ 07+OPJHfzMgbQy/+zmerf4Z1SagCx8G8sFXDwix+0wUH1Pdhc9wAWl38IE08w8BV4x4/TjDJQ4
+ H5C6pFW+OrbH557+JykR+8y0HMZ6GSl/41d6VYx9dpQ/9cjd+AvhXfwEOzVHuV3900i4BG61zY
+ V3kRLVA3yecdBMXQfhMmSivULwFhaAOKBT/70T5f/8T/DyV/w9GMdwuengqy8PK0B8lrvQlLDf
+ VhZ8RBmnP4Ixh19BJ6YAxWLL8Gxi49eGOh8Lal+LIDOrfS/Hr6DO4gqbah/PfMWNYDDmuoe5Xd
+ x8NXgHVcNPkTxaIAFyDD8AIQo0r7SLwBVhv0/M/2j7R8OfzwmH4M5G853agB+KiDwi+7fgX037
+ kFMzQOYf+4RjFLeBx/cRTx26aMXEjyn0x9+I4DOrfSBe24I8vvAPXgcfmcjE+ZXdT+/i4KPZ+S
+ bgMLBlxynrUEjDkLw8AOgl/8LgW8AQQE4n9n29cbpX0xanUkG4FIBMUBOy11IuHIfll38EMYV3
+ IO38J6+r7798YWGT/QQnyMH/S9H78Crx+7Cqx/cxeNddYLCznZbfa/kdz54yfvlIFldpgUv2YF
+ Kqgb/aYcgaFQGBI1Mh1C/5GjuDZw8yPv20A6APwDi9f/k0AcxwIEjzSYbgKSC+ItqSLt+D9aps
+ PA79RCiD9584cHz5VX6kEJ/tegu/Pn4XawFWno/v+uveIQuXYV7H28VaMFLk1E78b6KSXkQODo
+ TlQEhwakqxgATDkURA+DpH/EOAAtAct6vqwYgKm68B9suM23f/JcMPtH0usfw5xP34M8n78Gfi
+ u/hzS71fZbffdjV7ovQZdGnwG9CHsjWnaPgZSmoJBUEjM2GgDFZEIgHdxRhe//OGSBW1wKKdwD
+ kjH93DEC0sPAmjE+th9ra2pfOAHEtn8OfSu7Bf566jy2huk/zuy+72iW42mXzj+PuZz7IlxSDb
+ FcNyFNrwG/dWfDHrfuAyBxqhMCRGb9yLWCJeAuomwASA3hhndAdA5BUcPjwYVoUfvzxxy+VAao
+ +08B/nr6Pq/8uOMbUiOf3DV3L79rVrpffJWyYl+Jql087AvLJh0A+twhvvkH4u1ELisB/Ap7bH
+ J9LjYC7g8AZQMUZQLsHoNcCkhs93KafgB1ZjV02ANHjx4+pAUh7+P333780Bmj66ntqgMG7r/V
+ afteudr38TsP8LlQcHrSZegRkUw7jPsoh8NtTg7utlTQiUE3MY42AR/aD06KJAdoFBnhbzwCLG
+ QM4v1Nichsoprq6OmoC0h28TFHgb4daejW/09VOwOMNNdIdKhY8E+bJapfjfRYyjADUBCj/fWi
+ ANWeoGeRvFoAfEWsERcieeGIAYAzAHgLhG2CuzgD2C8pg1PzibhuAiIyIiQkePHjw0hjABUN/b
+ +Z3GuaJVpTiucpzWvA0zONql+G0TzatUCv/2HJdRGCjgnxyATVD8LD9KmoA7Taw/hSQM8Dyajy
+ pUkFHwU80P3XbAF988cVLlwpmZlzv1fzOrXjp/JMgW3pKC95/L/66A8P/9KOoQkY8I3ARgYgzQ
+ tDwg20mR4A3cKfKa9IRqLj8UY+iQGNj40uRCkjBW1FRAaPiyw3D/Nau5Xcpl99T2VCfVAXSmR/
+ godqTFDwJ8wH78evLT4P0bTQA0fSjOjMYMQIWgk86NsAcoQGcZ5dAUkZDjwzATwXGjpE9ryKHY
+ 8kmGNf1EI2KL+t6fhcUdrz8zoZ6+cZykEZ9QE1AwAccQKWiKWYcY4QG4CQwgsAMxAC5fAN0XAQ
+ OXFMLVisqYPTckz02AEkFJA0QdXSi+HkQSWW3b9/Wno3UV8SO8i7nd33wNMzzQr1sUTEaoIiaI
+ PBgDZX/Bhz6zDymM4EJRiDFoMAAujnAeeEcgDXAGxsvg+fkQnj8X5oem0CtVnd6oticRQpZEuL
+ FoBNjV1ZW0k2xOfnNXcvv8ZUGhR0N82yo90+sAOk7RVoFZdRC4B6VNiIw0jeCzgy69MAYAQ1wC
+ IxNApdn3dYOggauJgbAO2Kjz8PBwzd6bAD+iWL+MTJzD/FkoskP8WLQ+c9x7qHmLuV3KZ64EoB
+ n8zsX6mXRmOdnHWeEBgjOqgW/laXaiCA0wgdCI8wwNAIZBQOzF3CCdysYY4D2z3+Ao3WfMvf+r
+ 6qBN3An6/Ut9RC+sKRXDNDZiWJzCfGkVuHqFn2RYlYfOl8RB652mN9lvPwu33IRJJhi/VJUOvA
+ IPfAAG+ZxoCPB490ShC9hTRCUIowIRo1gJD3gbiBjAOFuIGMA7kncatdAeEoTvLEeDbAZz+1jW
+ 9PY/GmvmED/GJm5iOR17nSTGHRyEpqMuTt7fl3J79J3T+HdUsXgj+1cAAudKCi9loZ62fJSkMw
+ 5wZiACk0w74QgIgiNIGYGoRHIdjAw5wHYA6HTS7X3A+g/mbMtX8KsI+Rt3Rpg9O6GXjEA/xjZs
+ 94w4lo3Lip1Fzpf+v27sfwu33wBJPNLUGiA5AoITNeBD87EO6e2XaDRQTLnJGMCPSNwEYExwnE
+ TooLOABo8ESR6IKS3AJt6jOxZbBh1lNdJVCLFKklV3UpxP/4s7N/1wXNhfg/WB4tPge+CEmqCg
+ J2VEITQSX6n2o8mWljMGIBTB0YQmqFjI6AB8lXkQ530zwSSewKelgGe9oYRgW4sr/cUumDo1f6
+ NoH83AM/mdxnekua7kBiAMUHgrkoIzq4FBVEOhv5lpTQ1aNWpEU6YbATyeX5oAN44mDcMepoG6
+ OsNo476dbL6yYSyN6DzVXn/C+Pguf49vgJ8F51mDMAqMK2Kgifyw/6eSQ1MeujYCKakB6ER0AD
+ KWMNZAFMI3nr05KmboDenhBx0sWKOQCeGI0OpvnoumVceiYNn83tAmgokS0sZA3BCA3Dw/eMu0
+ ojApQaTjNDFOmGAc6QymmsF9c8F1rV+9dQNwE0Je9IaGhvSkJ/b19D52nrpjiF4LOpIfg/CP0u
+ Wn8XcX8qKMYB05VkKP2DHJUFU4NKD0AzdNwKRnAyCXCJzFbpOQHg0vK71y6duAP6UkEQDU+sBD
+ rp+BW9sQPM0FHWsiQVfqwVPiroQlHT1OfB9F++GereUEWsEv83nMS1cMogKXTKCiXUCnhr6YYD
+ L2BwL8nm+2kJQuy18Fi4/IwPwW0Nj9QA/vIu1bc8KurYD+OlnbRunreixqBuedxlm7LmM90aco
+ fIl4hlBsvSMICJ0zQim1AkntMJt4SfcBzu3i9UBKcfvPbMXkH+WkBsVc9V7bwxo+lqqD7+g/Ts
+ HnoT1oTl1cPcLDcxNqwefpWcZGTGCLjWImKGnRmDNgAdE1YwBIpUlgjqAvUP4WRqAf4CEC+Vi0
+ ElVby7Q+dpedVfbxhFNy6+nUYHuD+xGA7x3VmcCfSMYNUMHRuhGnYAGKKcGwDogmtQB3EDIjn2
+ PgHkp1575C8m9+URf9ep9pXAM9Rz85Au3BX/nE12GBuDUC0boZp0QOCpzA2OAsTkezuP4dQCTB
+ t7CGxnM4cUk+byvevW+0Nm7n1Hws4810pAvqA2+/xkNcI4xQUdGWHrGhPTQszohzCPOUnt/IM4
+ DNI4iacDcX2xz1LqLrdQEYn/XcPdL8Fl2jpExI3QUFXqpTpC9fex/BDeIYhpQatPAm0XMWBing
+ u1//74fai/qQNk98FlerjOBvhGiTTTCEp4RulEn4KHQx0IDjM2JpGlA0A2UQHnDp/3gelFvJde
+ BNzEAJ74RlokZoW/qBP9xyiMGbxOD3YBG1w0wxeCuorZ+cL2kb3/4GbxXnGdVLjTC8nPGo0If1
+ AnBij2BBgZwicxRCodCJ2Dqltp+eL2kkquPwXvleUZ8I6zohhG6XCec1o2b3yn6b9E3inIZm43
+ dQB448qIAGQ1/+90/+gH2gubsbwTvVRfQABc6NYIvgibVel/UCXhfYKPRN4zEYlDNFIO6KFB+9
+ ZN+gD097/DlD+C9+iJjAE58I+iZIWB4OkiwWOuLOiFo6P55xg0wNjtKPwqs2netH2IPFXO0hTG
+ AVhf0zKAzgl9kLgQF7+6TOkE+/ehPnb5pNEYBDRcFSEfgPKUIMg/mQI3q8nMxiDG71f8Vrv41l
+ 1hdNGoEAjlg2EEI9k8F/4jsPqkT8Hawo50bYGx2LIkCTEfAzAVWbVdqR7H9ZuiaNh5rBa+1l6g
+ ERuCZgYT7wND99BPXggJS6Xm9zgvGrs0TyABoiG+SZacGcB2TacFEAdIRHKY3jngNTYXF09dDc
+ kKaYC6feTAbzVDXb4YOVr/X+xWM1lbojMCawQcBy9/EO3UDd6PSqAECFXspXON1QrlhejChTvA
+ fl3vD5E8OcRmTHY1zAdBNBz8At/+YCu7/PhGG2EfBImqGVIEZMvrNYFj5ZzSB17pKnQl4RpDMO
+ gEBQw9AYNBu1gCMCfzG53VaJ3R1niDB3j9Ekqzo0odH4XSwXVcQFoKz1yZw++MEVuPBFRViPxP
+ NsK7fDCIqqP0YvNZXMgbQqgKGrzpHcz35jMXAIE46ExBgnRWMFC7mdFONEDA6q7XLnx7mOiZLg
+ akAaCogI+JxBQL4rn8cBy5/YOT8h0hQ2M+AhdPfFzVD9UtmBhL6g7bWogGqqIK21sDGotv06wE
+ Y4gOD9zIG0DOBfGK+0TqBbwS/cUoK1pTBkhSPf4VIdyq69QGSWBCW6FLBEXBxWCEKn9FYcPrfR
+ GMgyG46LJi2FpJeUjNM3nsNvDZUweQ9jVB87VM6BiZfv3b9Y2oAqmBDI3gvKzeoE/SNQODTLqH
+ DeQJrBCwAg0L2Xu32J4i6RmRYYCrQ0K6AmCAyD1z/7+QO4TtSRYAD0SsREGg7FeZPW/PSmOFS6
+ +eQX/cxrvYfDY+K51xF+Pt0JuBFAynerdtRwUhMQOoDUiiSM/0GgyWROoF8v0KWohjQkwu7gkg
+ 0AXCtoZPHBpPhO7wyGuypRlEF2EyB+VPRDPG7DMxQW1P/Py96ZJg5+ygEhOxjxDOC/7B0CpmrE
+ 4RF4yUK158MiAKZLqGjeQJnBHL+XyFPKemVj5HHekBJ6gFqgrG5/3L+07Quw7f7/UgqW6oR4G/
+ zFsybutrADAX5Bd9cqb/604tmhk8+/Rb8Q/brDMAzgQ+OamnBuF5YMBIDkA0c0i1wRSJ5d4/O6
+ gTS9wcF7f4u2G+XZa8YwC0i3QJNoNaaIGRXd+BrOPhENr8brpXc+k00wypISUr7mW+G/BfIDIU
+ fNNNPWCcK4BmBACX1AlcwckagIRzzPVcgUuH3E8Ad1Qk+mPsDyUTRb1fsgN683CIyLNEEGq0Jn
+ FZ2deVHI/gSffjWvwvXyuq3w2CUfHYSwk9FtfPNkJeX/1VtTZ3meTXD+CkFWgNQoQn8RmMxR8L
+ 8BhVjAhQBLMPhkFinQCaEHQ+WWPj+qeoBfXGhCRRoAqAmiMgCx7/M6ErYp/kIoSuNwSca/NuhG
+ pQH+V4E7yFmBqVS+fn58gs/Pq3bvHqqa02PwT/0ACPWAH4k7+Mq94xRgedGFYb6UtoGCopEXqf
+ gPyLTID0I6gTM+wHh6cw42T/VY0BfXZgOoqgJSGEYlgYO/2e8STkfpeF+BsKPQugaEficDBxsz
+ Aw52TkaczfDloQK8OMMwMoXb87wWXwGpFOOgN/QdNHagN8pkKmesTqBRA2yhczsJaTFDujrizF
+ BJjWBk1+cKfC5kB/J/QwE74FSi8CnsvxNWKqx/9+YGbLN0AyffPoE/MIOshKaQJAS9GoDvhFkU
+ 8XrBGICEkUChmdwRaJ6wNO63EYfjHKNICbIBidJrCnwiZT6PwfhR7Nhnw+fVWinPaxxM2R/d67
+ s/D+etRm2JFbyDMAqtAMj6JnAb1Q2eG1UCeoEzgzkgEcA/hu2SNSgASwHPM2LmADrAiApwdHhv
+ c7gc/neQv/nIHQLVCyqnQcfBv0mVIOyMPXxmJsZ6OrH8M5IzwQmRAN//Df8OsGL0wbmnUH1Rsm
+ RA57F5T7qQJTb6HTAqSE4Wi/sDD5RVEc/D+FHInwlgm8fRE0wRNmdx2XUDFlPzwwLV5wGORZ7c
+ q0J0rsUDcg5fs+Yap5UtMf3G5WlP0pOHfAsL/dR+yPdR+z7hRjByX0NdNLqdSlPDfzNkB5XtM/
+ CDJW1HyL8DJ7SuxQNpFMKwXNTDatq8MI2Tz6xwKBIRBOoBpjD5TFyr4dbeJoGIwI4ea+DTlo9x
+ bN6nE/DDORt9cNwDi8Pz2DEM4Ep0UA+Nhc8Y2uoyLavbNIhY1PEdpTFAHO5PEbssXAPTb7rPnI
+ fuAYngM0fRoORVk9pDo+3r8ywctN5kIVnIvxMERNkCE0gEg1855WAdOpR8MN/10GnoEF5DDDHy
+ 1WyMRvNAO7DdoHdX6eBkVbP0pwec2+Z4XT5HZANz0JlMjJqBL4J0rvaKZgvfO1ZAs/VI92D4v7
+ hOXw3OLksE+vzleb62LtrhrZ7X4BsRBYjE03QWTQQMYEGZd7wBUZwX1XsGZr0L/fgeLB99S39P
+ t/S3B+/MTNkZWb9UHa2/GfODCTvh04oQPjZeibo1WigQT0/8LnLyX7RIBfXZWqvYTvB2W05WP1
+ 7BNfnK5+n52HMDPv2Z/045u38f0lHZgMV3wS9Fw2eT/iCO5DtFwa4uq9QeQVu+aeT7XywfCWc9
+ PmWz+Nz4cywe0/64xFTEPqoHISfA0IT9Fo0UKMsBrxIl4vT4ng3j+XN9m9Mj3len4NkdK6FZFS
+ uWkLgc+KboHeigRKjwYsF/0W4JBFKCzSAGgVUfBP0JBoIh0fR/a+0GV6+EUoPNIAaBVoDUBPoG
+ WFkjhoNoOlGNFCjATz6X2lzhD8mT4HS+BL4fAmMQA1QgkrtRjRIRQP0h3yzhD82Lxrhg05K8BU
+ 3QTRGA48u1gZqNED/qjfHyycy38JnbH6J79h8QBOA0AR5fBOoJCQ9kOJwdG67ibWBBg0Q1f8qm
+ y38gkiUBk0APtQAnAQmaMdoEKUtDkl90HFtAGxtEIvqD/fmeHmPO2ThM66gBAVoAFb5IDQCAZ8
+ XxasPLHy54lC/LtBFAw0qFtUP3mzhjz8UjdKgCcCHim8CaoQSNEGkXnHogVJ3UBuQlBCL0aAfv
+ NmCn3BYgVJ7jz8MaABG4xix0SAKDWBhWBzmR2I00BipDUhd0J/jzfnymnhE4TXhiAoFaABG44k
+ RDqWiIlEWRuoDC4wGqSK1AUkNpGOw7H91zRn8pEJLr4mFqWgAFYr8Go0mUJhUHI6jxWE7rzZQo
+ xGi0QT9rdwLXh8oMCWofKgKUtkuoT+v91/P3/X/Afw1kptmVhryAAAAAElFTkSuQmCC
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/head.js b/comm/mailnews/addrbook/test/unit/head.js
new file mode 100644
index 0000000000..7a5155ea26
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/head.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+// Import the required setup scripts.
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+registerCleanupFunction(function () {
+ load("../../../resources/mailShutdown.js");
+});
+
+function promiseDirectoryRemoved(uri) {
+ let removePromise = TestUtils.topicObserved("addrbook-directory-deleted");
+ MailServices.ab.deleteAddressBook(uri);
+ return removePromise;
+}
+
+function acObserver() {}
+acObserver.prototype = {
+ _search: null,
+ _result: null,
+ _resolve: null,
+
+ onSearchResult(aSearch, aResult) {
+ this._search = aSearch;
+ this._result = aResult;
+ this._resolve();
+ },
+
+ waitForResult() {
+ return new Promise(resolve => {
+ this._resolve = resolve;
+ });
+ },
+};
+
+function formatVCard(strings, ...values) {
+ let arr = [];
+ for (let str of strings) {
+ arr.push(str);
+ arr.push(values.shift());
+ }
+ let lines = arr.join("").split("\n");
+ let indent = lines[1].length - lines[1].trimLeft().length;
+ let outLines = [];
+ for (let line of lines) {
+ if (line.length > 0) {
+ outLines.push(line.substring(indent) + "\r\n");
+ }
+ }
+ return outLines.join("");
+}
diff --git a/comm/mailnews/addrbook/test/unit/head_cardDAV.js b/comm/mailnews/addrbook/test/unit/head_cardDAV.js
new file mode 100644
index 0000000000..5c6ac62f61
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/head_cardDAV.js
@@ -0,0 +1,149 @@
+/* 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/. */
+
+const { CardDAVDirectory } = ChromeUtils.import(
+ "resource:///modules/CardDAVDirectory.jsm"
+);
+const { CardDAVServer } = ChromeUtils.import(
+ "resource://testing-common/CardDAVServer.jsm"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+Cu.importGlobalProperties(["fetch"]);
+
+do_get_profile();
+
+registerCleanupFunction(function () {
+ load("../../../resources/mailShutdown.js");
+});
+
+function initDirectory() {
+ // Set up a new directory and get the cards from the server. Do this by
+ // creating an instance of CardDAVDirectory rather than through the address
+ // book manager, so that we can access the internals of the directory.
+
+ Services.prefs.setIntPref("ldap_2.servers.carddav.carddav.syncinterval", 0);
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.carddav.url",
+ CardDAVServer.url
+ );
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.carddav.username",
+ "bob"
+ );
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.description",
+ "CardDAV Test"
+ );
+ Services.prefs.setIntPref(
+ "ldap_2.servers.carddav.dirType",
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE
+ );
+ Services.prefs.setStringPref(
+ "ldap_2.servers.carddav.filename",
+ "carddav.sqlite"
+ );
+
+ if (!Services.logins.findLogins(CardDAVServer.origin, null, "test").length) {
+ // Save a username and password to the login manager.
+ let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo.init(CardDAVServer.origin, null, "test", "bob", "bob", "", "");
+ Services.logins.addLogin(loginInfo);
+ }
+
+ let directory = new CardDAVDirectory();
+ directory.init("jscarddav://carddav.sqlite");
+ return directory;
+}
+
+async function clearDirectory(directory) {
+ await directory.cleanUp();
+
+ let database = do_get_profile();
+ database.append("carddav.sqlite");
+ database.remove(false);
+}
+
+async function checkCardsOnServer(expectedCards) {
+ // Send a request to the server. When the server responds, we know it has
+ // completed all earlier requests.
+ await fetch(`${CardDAVServer.origin}/ping`);
+
+ info("Checking cards on server are correct.");
+ let actualCards = [...CardDAVServer.cards];
+ Assert.equal(actualCards.length, Object.keys(expectedCards).length);
+
+ for (let [href, { etag, vCard }] of actualCards) {
+ let baseName = href
+ .substring(CardDAVServer.path.length)
+ .replace(/\.vcf$/, "");
+ info(baseName);
+ Assert.equal(etag, expectedCards[baseName].etag);
+ Assert.equal(href, expectedCards[baseName].href);
+ // Decode the vCard which is stored as UTF-8 on the server.
+ vCard = new TextDecoder().decode(
+ Uint8Array.from(vCard, c => c.charCodeAt(0))
+ );
+ vCardEqual(vCard, expectedCards[baseName].vCard);
+ }
+}
+
+let observer = {
+ notifications: {
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ },
+ pendingPromise: null,
+ init() {
+ if (this.isInited) {
+ return;
+ }
+ this.isInited = true;
+
+ for (let key of Object.keys(this.notifications)) {
+ Services.obs.addObserver(observer, key);
+ }
+ },
+ checkAndClearNotifications(expected) {
+ Assert.deepEqual(this.notifications, expected);
+ for (let array of Object.values(this.notifications)) {
+ array.length = 0;
+ }
+ },
+ observe(subject, topic) {
+ let uid = subject.QueryInterface(Ci.nsIAbCard).UID;
+ info(`${topic}: ${uid}`);
+ if (this.pendingPromise && this.pendingPromise.topic == topic) {
+ let promise = this.pendingPromise;
+ this.pendingPromise = null;
+ promise.resolve(uid);
+ return;
+ }
+ this.notifications[topic].push(uid);
+ },
+ waitFor(topic) {
+ return new Promise(resolve => {
+ this.pendingPromise = { resolve, topic };
+ });
+ },
+};
+
+add_task(async () => {
+ CardDAVServer.open("bob", "bob");
+ registerCleanupFunction(async () => {
+ await CardDAVServer.close();
+ });
+});
+
+// Checks two vCard strings have the same lines, in any order.
+// Not very smart but smart enough.
+function vCardEqual(lhs, rhs, message) {
+ let lhsLines = lhs.split("\r\n").sort();
+ let rhsLines = rhs.split("\r\n").sort();
+ Assert.deepEqual(lhsLines, rhsLines, message);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js b/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js
new file mode 100644
index 0000000000..ebcb746f21
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js
@@ -0,0 +1,101 @@
+/* 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/. */
+
+/**
+ * Tests for LDAPMessage.jsm.
+ */
+
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { LDAPResponse, SearchRequest } = ChromeUtils.import(
+ "resource:///modules/LDAPMessage.jsm"
+);
+
+/**
+ * Test filter string is converted to asn1 blocks correctly.
+ */
+add_task(function test_SearchRequest_filter() {
+ let req = new SearchRequest(
+ "ou=people,dc=planetexpress,dc=com",
+ Ci.nsILDAPURL.SCOPE_SUBTREE,
+ "(memberof=cn=ship_crew,ou=people,dc=planetexpress,dc=com)",
+ "",
+ 0,
+ 0
+ );
+ let filterBlock = req.protocolOp.valueBlock.value[6];
+ let [filterKeyBlock, filterValueBlock] = filterBlock.valueBlock.value;
+ let filterKey = new TextDecoder().decode(filterKeyBlock.valueBlock.valueHex);
+ let filterValue = new TextDecoder().decode(
+ filterValueBlock.valueBlock.valueHex
+ );
+ Assert.equal(filterKey, "memberof", "Filter key should be correct");
+ Assert.equal(
+ filterValue,
+ "cn=ship_crew,ou=people,dc=planetexpress,dc=com",
+ "Filter value should be correct"
+ );
+});
+
+/**
+ * Test extensibleMatch filter is encoded correctly.
+ */
+add_task(function test_extensibleMatchFilter() {
+ // Test data is from https://ldap.com/ldapv3-wire-protocol-reference-search/.
+ // filter string, BER payload, description
+ let filterBER = [
+ [
+ "(uid:dn:caseIgnoreMatch:=jdoe)",
+ "a91f810f6361736549676e6f72654d61746368820375696483046a646f658401ff",
+ "<type>:dn:<rule>:=<value>",
+ ],
+ ["(uid:=jdoe)", "a90b820375696483046a646f65", "<type>:=<value>"],
+ [
+ "(:caseIgnoreMatch:=foo)",
+ "a916810f6361736549676e6f72654d617463688303666f6f",
+ ":<rule>:=<value>",
+ ],
+ // This one is not directly from ldap.com, but assembled from the above cases.
+ [
+ "(uid:caseIgnoreMatch:=jdoe)",
+ "a91c810f6361736549676e6f72654d61746368820375696483046a646f65",
+ "<type>:<rule>:=<value>",
+ ],
+ ];
+ for (let [filter, ber, description] of filterBER) {
+ let req = new SearchRequest(
+ "ou=people,dc=planetexpress,dc=com",
+ Ci.nsILDAPURL.SCOPE_SUBTREE,
+ filter,
+ "",
+ 0,
+ 0
+ );
+ let filterBlock = req.protocolOp.valueBlock.value[6];
+ Assert.equal(
+ CommonUtils.bufferToHex(new Uint8Array(filterBlock.toBER())),
+ ber,
+ description
+ );
+ }
+});
+
+/**
+ * Test parsing to SearchResultReference works.
+ */
+add_task(function test_SearchResultReference() {
+ // A BER payload representing a SearchResultReference with two urls, test data
+ // is from https://ldap.com/ldapv3-wire-protocol-reference-search/.
+ let hex =
+ "306d020102736804326c6461703a2f2f6473312e6578616d706c652e636f6d3a3338392f64633d6578616d706c652c64633d636f6d3f3f7375623f04326c6461703a2f2f6473322e6578616d706c652e636f6d3a3338392f64633d6578616d706c652c64633d636f6d3f3f7375623f";
+ let res = LDAPResponse.fromBER(CommonUtils.hexToArrayBuffer(hex).buffer);
+
+ // Should be correctly parsed.
+ Assert.equal(res.constructor.name, "SearchResultReference");
+ Assert.deepEqual(res.result, [
+ "ldap://ds1.example.com:389/dc=example,dc=com??sub?",
+ "ldap://ds2.example.com:389/dc=example,dc=com??sub?",
+ ]);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js b/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js
new file mode 100644
index 0000000000..9e2d1cc97e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js
@@ -0,0 +1,66 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/**
+ * Test suite for nsILDAPSyncQuery.
+ */
+
+const { LDAPDaemon, LDAPHandlerFn } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Ldapd.jsm"
+);
+const { BinaryServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Binaryd.jsm"
+);
+
+const nsILDAPSyncQuery = Ci.nsILDAPSyncQuery;
+const LDAPSyncQueryContractID = "@mozilla.org/ldapsyncquery;1";
+
+function getLDAPAttributes(urlSpec) {
+ let url = Services.io.newURI(urlSpec).QueryInterface(Ci.nsILDAPURL);
+ let ldapquery = Cc[LDAPSyncQueryContractID].createInstance(nsILDAPSyncQuery);
+ let payload = ldapquery.getQueryResults(url, Ci.nsILDAPConnection.VERSION3);
+ // Returns a string with one attr per line.
+ return payload;
+}
+
+add_task(async function test_LDAPSyncQuery() {
+ // Set up fake LDAP server, loaded with some contacts.
+ let daemon = new LDAPDaemon();
+ let raw = await IOUtils.readUTF8(
+ do_get_file(
+ "../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json"
+ ).path
+ );
+ let testContacts = JSON.parse(raw);
+ daemon.add(...Object.values(testContacts));
+ // daemon.setDebug(true);
+
+ let server = new BinaryServer(LDAPHandlerFn, daemon);
+ server.start();
+
+ // Fetch only the Holmes family.
+ let out = getLDAPAttributes(
+ `ldap://localhost:${server.port}/??sub?(sn=Holmes)`
+ );
+ if (daemon.debug) {
+ dump(`--- getLDAPAttributes() ---\n${out}\n--------------------\n`);
+ }
+
+ // Make sure we got the contacts we expected:
+ Assert.ok(out.includes("cn=Eurus Holmes"));
+ Assert.ok(out.includes("cn=Mycroft Holmes"));
+ Assert.ok(out.includes("cn=Sherlock Holmes"));
+
+ // Sanity check: make sure some non-Holmes people were excluded.
+ Assert.ok(!out.includes("cn=John Watson"));
+ Assert.ok(!out.includes("cn=Jim Moriarty"));
+
+ // Fetch again but this time the filter is without parens.
+ out = getLDAPAttributes(`ldap://localhost:${server.port}/??sub?sn=Holmes`);
+
+ // Make sure we got the contacts we expected:
+ Assert.ok(out.includes("cn=Eurus Holmes"));
+ Assert.ok(out.includes("cn=Mycroft Holmes"));
+ Assert.ok(out.includes("cn=Sherlock Holmes"));
+
+ server.stop();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_abCardProperty.js b/comm/mailnews/addrbook/test/unit/test_abCardProperty.js
new file mode 100644
index 0000000000..5feae230dc
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_abCardProperty.js
@@ -0,0 +1,178 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic nsIAbCard functions.
+ */
+
+// Intersperse these with UTF-8 values to check we handle them correctly.
+var kFNValue = "testFirst\u00D0";
+var kLNValue = "testLast";
+var kDNValue = "testDisplay\u00D1";
+var kEmailValue = "testEmail\u00D2@foo.invalid";
+var kEmailValueLC = "testemail\u00D2@foo.invalid";
+var kEmailValue2 = "test@test.foo.invalid";
+// Email without the @ or anything after it.
+var kEmailReducedValue = "testEmail\u00D2";
+var kCompanyValue = "Test\u00D0 Company";
+
+add_task(function testAbCardProperty() {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ // Test - Set First, Last and Display Names and Email Address
+ // via setProperty, and check correctly saved via their
+ // attributes. We're using firstName to check UTF-8 values.
+ card.setProperty("FirstName", kFNValue);
+ card.setProperty("LastName", kLNValue);
+ card.setProperty("DisplayName", kDNValue);
+ card.setProperty("PrimaryEmail", kEmailValue);
+
+ Assert.equal(card.firstName, kFNValue);
+ Assert.equal(card.lastName, kLNValue);
+ Assert.equal(card.displayName, kDNValue);
+ Assert.equal(card.primaryEmail, kEmailValue);
+
+ // Repeat in the opposite order.
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+ card.displayName = kDNValue;
+ card.primaryEmail = kEmailValue;
+
+ Assert.equal(card.getProperty("FirstName", "BAD"), kFNValue);
+ Assert.equal(card.getProperty("LastName", "BAD"), kLNValue);
+ Assert.equal(card.getProperty("DisplayName", "BAD"), kDNValue);
+ Assert.equal(card.getProperty("PrimaryEmail", "BAD"), kEmailValue);
+
+ // Test - generateName. Note: if the addressBook.properties
+ // value changes, this will affect these tests.
+
+ const {
+ GENERATE_DISPLAY_NAME,
+ GENERATE_LAST_FIRST_ORDER,
+ GENERATE_FIRST_LAST_ORDER,
+ } = Ci.nsIAbCard;
+
+ // Add a company name, so we can test fallback to company name.
+ card.setProperty("Company", kCompanyValue);
+
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kDNValue);
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kLNValue + ", " + kFNValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kFNValue + " " + kLNValue
+ );
+
+ // Test - generateName, with missing items.
+
+ card.displayName = "";
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kCompanyValue);
+
+ card.deleteProperty("Company");
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kEmailReducedValue);
+
+ // Reset company name for the first/last name tests.
+ card.setProperty("Company", kCompanyValue);
+
+ card.firstName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kLNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kLNValue);
+
+ card.firstName = kFNValue;
+ card.lastName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kFNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kFNValue);
+
+ card.firstName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kCompanyValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kCompanyValue);
+
+ card.deleteProperty("Company");
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kEmailReducedValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kEmailReducedValue
+ );
+
+ card.primaryEmail = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), "");
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), "");
+
+ // Test - generateNameWithBundle, most of this will have
+ // been tested above.
+
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+
+ Assert.equal(card.generateName(1, bundle), kLNValue + ", " + kFNValue);
+
+ // Test - generatePhoneticName
+
+ card.setProperty("PhoneticFirstName", kFNValue);
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kFNValue + kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue + kFNValue);
+
+ card.setProperty("PhoneticLastName", "");
+ Assert.equal(card.generatePhoneticName(false), kFNValue);
+ Assert.equal(card.generatePhoneticName(true), kFNValue);
+
+ card.setProperty("PhoneticFirstName", "");
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue);
+
+ // Test - emailAddresses
+
+ card.deleteProperty("PrimaryEmail");
+ card.deleteProperty("SecondEmail");
+ Assert.deepEqual(card.emailAddresses, []);
+
+ card.primaryEmail = kEmailValue;
+ Assert.deepEqual(card.emailAddresses, [kEmailValue]);
+
+ card.setProperty("SecondEmail", kEmailValue2);
+ Assert.deepEqual(card.emailAddresses, [kEmailValue, kEmailValue2]);
+
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, [kEmailValue2]);
+
+ card.deleteProperty("SecondEmail");
+ Assert.deepEqual(card.emailAddresses, []);
+
+ // Test - hasEmailAddress
+
+ card.deleteProperty("PrimaryEmail");
+ card.deleteProperty("SecondEmail");
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.setProperty("PrimaryEmail", kEmailValue);
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.setProperty("SecondEmail", kEmailValue2);
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+
+ card.deleteProperty("PrimaryEmail");
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_addrBookCard.js b/comm/mailnews/addrbook/test/unit/test_addrBookCard.js
new file mode 100644
index 0000000000..bf5a12b1dd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_addrBookCard.js
@@ -0,0 +1,260 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic nsIAbCard functions.
+ */
+
+const { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+const { VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+// Intersperse these with UTF-8 values to check we handle them correctly.
+var kFNValue = "testFirst\u00D0";
+var kLNValue = "testLast";
+var kDNValue = "testDisplay\u00D1";
+var kEmailValue = "testEmail\u00D2@foo.invalid";
+var kEmailValueLC = "testemail\u00D2@foo.invalid";
+var kEmailValue2 = "test@test.foo.invalid";
+// Email without the @ or anything after it.
+var kEmailReducedValue = "testEmail\u00D2";
+
+add_task(function testAddrBookCard() {
+ let card = new AddrBookCard();
+
+ // Test - Set First, Last and Display Names and Email Address
+ // via setProperty, and check correctly saved via their
+ // attributes. We're using firstName to check UTF-8 values.
+ card.vCardProperties.addValue("n", [kLNValue, kFNValue, "", "", ""]);
+ card.vCardProperties.addValue("fn", kDNValue);
+ card.vCardProperties.addValue("email", kEmailValue);
+
+ Assert.equal(card.firstName, kFNValue);
+ Assert.equal(card.lastName, kLNValue);
+ Assert.equal(card.displayName, kDNValue);
+ Assert.equal(card.primaryEmail, kEmailValue);
+
+ // Repeat in the opposite order.
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+ card.displayName = kDNValue;
+ card.primaryEmail = kEmailValue;
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [
+ kLNValue,
+ kFNValue,
+ "",
+ "",
+ "",
+ ]);
+ Assert.equal(card.vCardProperties.getFirstValue("fn"), kDNValue);
+ Assert.equal(card.vCardProperties.getFirstValue("email"), kEmailValue);
+
+ // Test - generateName. Note: if the addressBook.properties
+ // value changes, this will affect these tests.
+
+ const {
+ GENERATE_DISPLAY_NAME,
+ GENERATE_LAST_FIRST_ORDER,
+ GENERATE_FIRST_LAST_ORDER,
+ } = Ci.nsIAbCard;
+
+ Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kDNValue);
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kLNValue + ", " + kFNValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kFNValue + " " + kLNValue
+ );
+
+ // Test - generateName, with missing items.
+
+ card.displayName = "";
+ Assert.equal(
+ card.generateName(GENERATE_DISPLAY_NAME),
+ kFNValue + " " + kLNValue
+ );
+
+ card.firstName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kLNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kLNValue);
+
+ card.firstName = kFNValue;
+ card.lastName = "";
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kFNValue);
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kFNValue);
+
+ card.firstName = "";
+ Assert.equal(
+ card.generateName(GENERATE_LAST_FIRST_ORDER),
+ kEmailReducedValue
+ );
+ Assert.equal(
+ card.generateName(GENERATE_FIRST_LAST_ORDER),
+ kEmailReducedValue
+ );
+
+ card.vCardProperties.clearValues("email");
+ Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), "");
+ Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), "");
+
+ // Test - generateNameWithBundle, most of this will have
+ // been tested above.
+
+ card.firstName = kFNValue;
+ card.lastName = kLNValue;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/addressbook/addressBook.properties"
+ );
+
+ Assert.equal(card.generateName(1, bundle), kLNValue + ", " + kFNValue);
+
+ // Test - generatePhoneticName
+
+ card.setProperty("PhoneticFirstName", kFNValue);
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kFNValue + kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue + kFNValue);
+
+ card.setProperty("PhoneticLastName", "");
+ Assert.equal(card.generatePhoneticName(false), kFNValue);
+ Assert.equal(card.generatePhoneticName(true), kFNValue);
+
+ card.setProperty("PhoneticFirstName", "");
+ card.setProperty("PhoneticLastName", kLNValue);
+ Assert.equal(card.generatePhoneticName(false), kLNValue);
+ Assert.equal(card.generatePhoneticName(true), kLNValue);
+
+ // Test - emailAddresses
+
+ Assert.deepEqual(card.emailAddresses, []);
+
+ card.primaryEmail = kEmailValue;
+ Assert.deepEqual(card.emailAddresses, [kEmailValue]);
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", kEmailValue2)
+ );
+ Assert.deepEqual(card.emailAddresses, [kEmailValue, kEmailValue2]);
+
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, [kEmailValue2]);
+
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, []);
+
+ // Test - primaryEmail
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", "three@invalid")
+ );
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", { pref: 2 }, "text", "two@invalid")
+ );
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", "four@invalid")
+ );
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", { pref: 1 }, "text", "one@invalid")
+ );
+ Assert.deepEqual(card.emailAddresses, [
+ "one@invalid",
+ "two@invalid",
+ "three@invalid",
+ "four@invalid",
+ ]);
+ Assert.equal(card.primaryEmail, "one@invalid");
+
+ // Setting primaryEmail to the existing value changes nothing.
+ card.primaryEmail = "one@invalid";
+ Assert.deepEqual(card.emailAddresses, [
+ "one@invalid",
+ "two@invalid",
+ "three@invalid",
+ "four@invalid",
+ ]);
+ Assert.equal(card.primaryEmail, "one@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", "2", undefined, undefined]
+ );
+
+ // Setting primaryEmail to another existing address replaces the address with the new one.
+ card.primaryEmail = "four@invalid";
+ Assert.deepEqual(card.emailAddresses, [
+ "four@invalid",
+ "two@invalid",
+ "three@invalid",
+ ]);
+ Assert.equal(card.primaryEmail, "four@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", "2", undefined]
+ );
+
+ // Setting primaryEmail to null promotes the next address.
+ card.primaryEmail = null;
+ Assert.deepEqual(card.emailAddresses, ["two@invalid", "three@invalid"]);
+ Assert.equal(card.primaryEmail, "two@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", undefined]
+ );
+
+ // Setting primaryEmail to a new address replaces the address with the new one.
+ card.primaryEmail = "five@invalid";
+ Assert.deepEqual(card.emailAddresses, ["five@invalid", "three@invalid"]);
+ Assert.equal(card.primaryEmail, "five@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1", undefined]
+ );
+
+ // Setting primaryEmail to an empty string promotes the next address.
+ card.primaryEmail = "";
+ Assert.deepEqual(card.emailAddresses, ["three@invalid"]);
+ Assert.equal(card.primaryEmail, "three@invalid");
+ Assert.deepEqual(
+ card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref),
+ ["1"]
+ );
+
+ // Setting primaryEmail to null clears the only address.
+ card.primaryEmail = null;
+ Assert.deepEqual(card.emailAddresses, []);
+ Assert.equal(card.primaryEmail, "");
+
+ // Test - hasEmailAddress
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", kEmailValue)
+ );
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), false);
+
+ card.vCardProperties.addEntry(
+ new VCardPropertyEntry("email", {}, "text", kEmailValue2)
+ );
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), true);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), true);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+
+ card.vCardProperties.removeEntry(
+ card.vCardProperties.getAllEntries("email")[0]
+ );
+
+ Assert.equal(card.hasEmailAddress(kEmailValue), false);
+ Assert.equal(card.hasEmailAddress(kEmailValueLC), false);
+ Assert.equal(card.hasEmailAddress(kEmailValue2), true);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js b/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js
new file mode 100644
index 0000000000..1b76099227
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic address book functions - tests obtaining the (default)
+ * personal address book and getting its details from the nsIAbDirectory.
+ *
+ * Functions/attributes not currently tested:
+ * - lastModifiedDate
+ * - childNodes
+ * - childCards
+ * - deleteDirectory
+ * - hasCard
+ * - hasDirectory
+ * - addCard
+ * - modifyCard
+ * - deleteCards
+ * - dropCard
+ * - addressLists
+ * - addMailList
+ * - listNickName
+ * - description
+ * - editMailListToDatabase
+ * - copyMailList
+ */
+
+// Main function for the this test so we can check both personal and
+// collected books work correctly in an easy manner.
+function check_ab(abConfig) {
+ // Test - Get the directory
+
+ let AB = MailServices.ab.getDirectory(abConfig.URI);
+
+ // Test - Is it the right type?
+
+ if (abConfig.dirType == 2) {
+ Assert.ok(AB instanceof Ci.nsIAbMDBDirectory);
+ }
+
+ // Test - Check attributes
+
+ Assert.equal(AB.propertiesChromeURI, kNormalPropertiesURI);
+ Assert.equal(AB.readOnly, abConfig.readOnly);
+ Assert.equal(AB.dirName, abConfig.dirName);
+ Assert.equal(AB.dirType, abConfig.dirType);
+ Assert.equal(AB.fileName, abConfig.fileName);
+ Assert.equal(AB.URI, abConfig.URI);
+ Assert.equal(AB.position, abConfig.position);
+ Assert.equal(AB.isMailList, false);
+ Assert.equal(AB.isRemote, false);
+ Assert.equal(AB.isSecure, false);
+ Assert.equal(AB.supportsMailingLists, true);
+ Assert.equal(AB.dirPrefId, abConfig.dirPrefID);
+
+ // Test - autocomplete enable/disable
+
+ // enable is the default
+ Assert.equal(AB.useForAutocomplete(""), true);
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", false);
+ Assert.equal(AB.useForAutocomplete(""), false);
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", true);
+ Assert.equal(AB.useForAutocomplete(""), true);
+
+ AB.setBoolValue("enable_autocomplete", false);
+ Assert.equal(AB.useForAutocomplete(""), false);
+
+ AB.setBoolValue("enable_autocomplete", true);
+ Assert.equal(AB.useForAutocomplete(""), true);
+
+ // Test - check getting default preferences
+
+ Assert.equal(AB.getIntValue("random", 54321), 54321);
+ Assert.equal(AB.getBoolValue("random", false), false);
+ Assert.equal(AB.getStringValue("random", "abc"), "abc");
+ Assert.equal(AB.getLocalizedStringValue("random", "xyz"), "xyz");
+
+ // Test - check get/set int preferences on nsIAbDirectory
+
+ AB.setIntValue("inttest", 12345);
+ Assert.equal(
+ Services.prefs.getIntPref(abConfig.dirPrefID + ".inttest"),
+ 12345
+ );
+ Assert.equal(AB.getIntValue("inttest", -1), 12345);
+
+ AB.setIntValue("inttest", 123456);
+ Assert.equal(
+ Services.prefs.getIntPref(abConfig.dirPrefID + ".inttest"),
+ 123456
+ );
+ Assert.equal(AB.getIntValue("inttest", -2), 123456);
+
+ // Test - check get/set bool preferences on nsIAbDirectory
+
+ AB.setBoolValue("booltest", true);
+ Assert.equal(
+ Services.prefs.getBoolPref(abConfig.dirPrefID + ".booltest"),
+ true
+ );
+ Assert.equal(AB.getBoolValue("booltest", false), true);
+
+ AB.setBoolValue("booltest", false);
+ Assert.equal(
+ Services.prefs.getBoolPref(abConfig.dirPrefID + ".booltest"),
+ false
+ );
+ Assert.equal(AB.getBoolValue("booltest", true), false);
+
+ // Test - check get/set string preferences on nsIAbDirectory
+
+ AB.setStringValue("stringtest", "tyu");
+ Assert.equal(
+ Services.prefs.getCharPref(abConfig.dirPrefID + ".stringtest"),
+ "tyu"
+ );
+ Assert.equal(AB.getStringValue("stringtest", ""), "tyu");
+}
+
+function run_test() {
+ // Check the default personal address book
+ check_ab(kPABData);
+
+ // Check the default collected address book
+ check_ab(kCABData);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_bug1522453.js b/comm/mailnews/addrbook/test/unit/test_bug1522453.js
new file mode 100644
index 0000000000..b2da2eba12
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug1522453.js
@@ -0,0 +1,72 @@
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+add_task(async function () {
+ do_get_profile();
+ MailServices.ab.directories;
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "list";
+ list = book.addMailList(list);
+
+ let contact1 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact1.firstName = "contact";
+ contact1.lastName = "1";
+ contact1.primaryEmail = "contact1@invalid";
+ contact1 = book.addCard(contact1);
+ list.addCard(contact1);
+
+ let contact2 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact2.firstName = "contact";
+ contact2.lastName = "2";
+ // No email address!
+ contact2 = book.addCard(contact2);
+ list.addCard(contact2);
+
+ let contact3 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact3.firstName = "contact";
+ contact3.lastName = "3";
+ contact3.primaryEmail = "contact3@invalid";
+ contact3 = book.addCard(contact3);
+ list.addCard(contact3);
+
+ // book.childCards should contain the list and all three contacts.
+ let bookCards = book.childCards;
+ equal(bookCards.length, 1 + 3);
+ equal(list.UID, bookCards[0].UID);
+ equal(contact1.UID, bookCards[1].UID);
+ equal(contact2.UID, bookCards[2].UID);
+ equal(contact3.UID, bookCards[3].UID);
+
+ // list.childCards should contain contacts 1 and 3, and crucially, not die at 2.
+ let listCards = list.childCards;
+ equal(listCards.length, 2);
+ equal(contact1.UID, listCards[0].UID);
+ equal(contact3.UID, listCards[1].UID);
+
+ // Reload the address book manager.
+ let reloadPromise = TestUtils.topicObserved("addrbook-reloaded");
+ Services.obs.notifyObservers(null, "addrbook-reload");
+ await reloadPromise;
+
+ // Renew our references.
+ book = MailServices.ab.getDirectory(kPABData.URI);
+ list = book.childNodes[0];
+
+ // list.childCards should contain contacts 1 and 3.
+ listCards = list.childCards;
+ equal(listCards.length, 2);
+ equal(contact1.UID, listCards[0].UID);
+ equal(contact3.UID, listCards[1].UID);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_bug1769889.js b/comm/mailnews/addrbook/test/unit/test_bug1769889.js
new file mode 100644
index 0000000000..37cc91fa45
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug1769889.js
@@ -0,0 +1,95 @@
+/* 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/. */
+
+/**
+ * Tests that complex names are correctly flattened when stored in the
+ * database as FirstName/LastName, and when returned from the
+ * firstName/lastName getters.
+ */
+
+var { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+
+add_task(async function testMultiValueLast() {
+ // Multiple last names.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:second-last,last;first;;;
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [
+ ["second-last", "last"],
+ "first",
+ "",
+ "",
+ "",
+ ]);
+ Assert.equal(card.firstName, "first");
+ Assert.equal(card.getProperty("FirstName", "WRONG"), "first");
+ Assert.equal(card.lastName, "second-last last");
+ Assert.equal(card.getProperty("LastName", "WRONG"), "second-last last");
+});
+
+add_task(async function testMultiValueFirst() {
+ // Multiple first names.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:last;first,second;;;
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [
+ "last",
+ ["first", "second"],
+ "",
+ "",
+ "",
+ ]);
+ Assert.equal(card.firstName, "first second");
+ Assert.equal(card.getProperty("FirstName", "WRONG"), "first second");
+ Assert.equal(card.lastName, "last");
+ Assert.equal(card.getProperty("LastName", "WRONG"), "last");
+});
+
+add_task(async function testNotEnoughValues() {
+ // The name field doesn't have enough components. That's okay.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:last;first
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), ["last", "first"]);
+ Assert.equal(card.firstName, "first");
+ Assert.equal(card.getProperty("FirstName", "WRONG"), "first");
+ Assert.equal(card.lastName, "last");
+ Assert.equal(card.getProperty("LastName", "WRONG"), "last");
+});
+
+add_task(async function testStringValue() {
+ // This is a bad value. Let's just ignore it for first/last name purposes.
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ N:first last
+ END:VCARD
+ `;
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = book.addCard(VCardUtils.vCardToAbCard(vCard));
+
+ Assert.deepEqual(card.vCardProperties.getFirstValue("n"), "first last");
+ Assert.equal(card.firstName, "");
+ Assert.equal(card.getProperty("FirstName", "RIGHT"), "RIGHT");
+ Assert.equal(card.lastName, "");
+ Assert.equal(card.getProperty("LastName", "RIGHT"), "RIGHT");
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_bug387403.js b/comm/mailnews/addrbook/test/unit/test_bug387403.js
new file mode 100644
index 0000000000..9f8621c705
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug387403.js
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Test for bug 387403 crash when opening e-mail with broken vcard.
+ */
+
+function run_test() {
+ // Before bug 387403 this would hang, eating up all the memory until it
+ // crashed.
+ try {
+ Cc["@mozilla.org/addressbook/msgvcardservice;1"]
+ .getService(Ci.nsIMsgVCardService)
+ .escapedVCardToAbCard(
+ "begin:vcard\nfn;quoted-printable:Xxxx=C5=82xx Xxx\nn;quoted-printable:Xxx;Xxxx=C5=82xx \nadr;quoted-printable;quoted-printable;dom:;;xx. Xxxxxxxxxxxx X;Xxxxxx=C3=3"
+ );
+ } catch (ex) {}
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_bug448165.js b/comm/mailnews/addrbook/test/unit/test_bug448165.js
new file mode 100644
index 0000000000..57337f7fa1
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug448165.js
@@ -0,0 +1,18 @@
+/**
+ * A simple test to check for a regression of bug 448165: Mailnews crashes in
+ * nsAbMDBDirectory::DeleteCards if aCards is null
+ */
+function run_test() {
+ // get the Personal Address Book
+ let pab = MailServices.ab.getDirectory(kPABData.URI);
+ Assert.ok(pab instanceof Ci.nsIAbDirectory);
+ try {
+ pab.deleteCards(null); // this should throw an error
+ do_throw(
+ "Error, deleteCards should throw an error when null is passed to it"
+ );
+ } catch (e) {
+ // make sure the correct error message was thrown
+ Assert.equal(e.result, Cr.NS_ERROR_XPC_CANT_CONVERT_PRIMITIVE_TO_ARRAY);
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_bug534822.js b/comm/mailnews/addrbook/test/unit/test_bug534822.js
new file mode 100644
index 0000000000..4c18f64b5d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_bug534822.js
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests for bug 534822 - non-built-in address books specified in preferences
+ * don't appear in address book lists.
+ */
+
+function run_test() {
+ // Read in the prefs that will be default.
+ let specialPrefs = do_get_file("data/bug534822prefs.js");
+
+ var profileDir = do_get_profile();
+ specialPrefs.copyTo(profileDir, "");
+
+ specialPrefs = profileDir;
+ specialPrefs.append("bug534822prefs.js");
+
+ Services.prefs.readUserPrefsFromFile(specialPrefs);
+
+ // Now load the ABs and check we've got all of them.
+ let results = [
+ { name: "extension", result: false },
+ { name: kPABData.dirName, result: false },
+ { name: kCABData.dirName, result: false },
+ ];
+
+ for (let dir of MailServices.ab.directories) {
+ for (let i = 0; i < results.length; ++i) {
+ if (results[i].name == dir.dirName) {
+ Assert.ok(!results[i].result);
+ results[i].result = true;
+ }
+ }
+ }
+
+ results.forEach(function (result) {
+ Assert.ok(result.result);
+ });
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js
new file mode 100644
index 0000000000..1e8e476c7a
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const vCardTemplate =
+ "BEGIN:VCARD\r\nUID:{}\r\nFN:Move me around\r\nEND:VCARD\r\n";
+const initialVCard = vCardTemplate.replace("{}", "copyme");
+
+let cardDAVDirectory, localDirectory;
+let initialCard, localCard;
+
+add_task(async () => {
+ // Put some cards on the server.
+
+ CardDAVServer.putCardInternal("copyme.vcf", initialVCard);
+
+ localDirectory = MailServices.ab.getDirectoryFromId("ldap_2.servers.pab");
+ cardDAVDirectory = initDirectory();
+ await cardDAVDirectory.fetchAllFromServer();
+
+ observer.init();
+
+ // Check we have the initial version of the card.
+
+ Assert.equal(cardDAVDirectory.childCards.length, 1);
+
+ initialCard = cardDAVDirectory.childCards[0];
+ Assert.equal(initialCard.UID, "copyme");
+ Assert.equal(initialCard.getProperty("_etag", ""), "55");
+ Assert.equal(
+ initialCard.getProperty("_href", ""),
+ `${CardDAVServer.path}copyme.vcf`
+ );
+ vCardEqual(initialCard.getProperty("_vCard", ""), initialVCard);
+});
+
+/** Copy the card to the local directory. */
+add_task(async function copyCardToLocal() {
+ localDirectory.dropCard(initialCard, true);
+ Assert.equal(localDirectory.childCards.length, 1);
+
+ localCard = localDirectory.childCards[0];
+ // The UID must change, since this is a copy.
+ Assert.notEqual(localCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ vCardEqual(
+ localCard.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", localCard.UID)
+ );
+});
+
+/** Remove the card from the local directory for the next step. */
+add_task(async function () {
+ localDirectory.deleteCards(localDirectory.childCards);
+ Assert.equal(localDirectory.childCards.length, 0);
+});
+
+/** This time, move the card to the local directory. */
+add_task(async function moveCardToLocal() {
+ localDirectory.addCard(initialCard);
+ Assert.equal(localDirectory.childCards.length, 1);
+
+ localCard = localDirectory.childCards[0];
+ // UID should not change
+ Assert.equal(localCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ vCardEqual(
+ localCard.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", localCard.UID)
+ );
+});
+
+/**
+ * Okay, let's go back again. First we'll need to remove the card from the
+ * CardDAV directory.
+ */
+add_task(async function () {
+ let deletedPromise = observer.waitFor("addrbook-contact-deleted");
+ cardDAVDirectory.deleteCards(cardDAVDirectory.childCards);
+ await deletedPromise;
+ Assert.equal(cardDAVDirectory.childCards.length, 0);
+});
+
+/** Copy the card back to the CardDAV directory. */
+add_task(async function copyCardToCardDAV() {
+ cardDAVDirectory.dropCard(localCard, true);
+ Assert.equal(cardDAVDirectory.childCards.length, 1);
+
+ let newCard = cardDAVDirectory.childCards[0];
+ Assert.notEqual(newCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ vCardEqual(
+ localCard.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", localCard.UID)
+ );
+
+ await observer.waitFor("addrbook-contact-updated");
+ let newCardAfterSync = cardDAVDirectory.childCards[0];
+ Assert.equal(newCardAfterSync.getProperty("_etag", "EMPTY"), "85");
+ Assert.equal(
+ newCardAfterSync.getProperty("_href", "EMPTY"),
+ `${CardDAVServer.path}${newCard.UID}.vcf`
+ );
+ vCardEqual(
+ newCardAfterSync.getProperty("_vCard", "EMPTY"),
+ vCardTemplate.replace("{}", newCard.UID)
+ );
+});
+
+/** Remove the card from the CardDAV directory again. */
+add_task(async function () {
+ let deletedPromise = observer.waitFor("addrbook-contact-deleted");
+ cardDAVDirectory.deleteCards(cardDAVDirectory.childCards);
+ await deletedPromise;
+ Assert.equal(cardDAVDirectory.childCards.length, 0);
+});
+
+/** This time, move the card to the CardDAV directory. */
+add_task(async function moveCardToCardDAV() {
+ cardDAVDirectory.addCard(localCard);
+ Assert.equal(cardDAVDirectory.childCards.length, 1);
+
+ let newCard = cardDAVDirectory.childCards[0];
+ // UID should not change
+ Assert.equal(newCard.UID, "copyme");
+ Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY");
+ Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY");
+ // _vCard property won't change until we send this card to the server.
+ vCardEqual(localCard.getProperty("_vCard", "EMPTY"), initialVCard);
+
+ await observer.waitFor("addrbook-contact-updated");
+ let newCardAfterSync = cardDAVDirectory.childCards[0];
+ Assert.equal(newCardAfterSync.getProperty("_etag", "EMPTY"), "55");
+ Assert.equal(
+ newCardAfterSync.getProperty("_href", "EMPTY"),
+ `${CardDAVServer.path}copyme.vcf`
+ );
+ vCardEqual(newCardAfterSync.getProperty("_vCard", "EMPTY"), initialVCard);
+
+ await clearDirectory(cardDAVDirectory);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js
new file mode 100644
index 0000000000..fa32260328
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js
@@ -0,0 +1,550 @@
+/* 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/. */
+
+// Tests that changes in a CardDAV directory when offline or unable to reach
+// the server are (a) visible in the client immediately, and (b) sent to the
+// server when it's next available.
+//
+// Note that we close the server rather than using Services.io.offline, as
+// the server is localhost and therefore not affected by the offline setting.
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+var directory, restart, useSyncV1;
+
+async function subtest() {
+ // Put some cards on the server.
+
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I shall be changed.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "delete-me.vcf",
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n"
+ );
+
+ directory = initDirectory();
+
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ if (useSyncV1) {
+ directory._syncToken = null;
+ }
+
+ await subtestCreateCard();
+ await subtestUpdateCard();
+ await subtestDeleteCard();
+ await subtestCreateDeleteCard();
+ await subtestStillOffline();
+
+ // Check everything is still correct at the end.
+
+ info("Checking cards on client are correct.");
+ Assert.deepEqual(
+ directory.childCards.map(c => c.UID).sort(),
+ ["another-new-card", "change-me"],
+ "right cards remain on client"
+ );
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+}
+
+function promiseSyncFailed() {
+ return TestUtils.topicObserved("addrbook-directory-sync-failed");
+}
+
+function promiseSyncSucceeded() {
+ return TestUtils.topicObserved("addrbook-directory-synced");
+}
+
+/**
+ * The behaviour should remain the same even if Thunderbird restarts.
+ * If `restart` is true, simulate restarting.
+ */
+async function pretendToRestart() {
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ if (!restart) {
+ return;
+ }
+
+ await directory.cleanUp();
+
+ info("Shutdown simulated, now restarting.");
+ directory = new CardDAVDirectory();
+ directory.init("jscarddav://carddav.sqlite");
+}
+
+/** Creating a new card while "offline". */
+async function subtestCreateCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test");
+
+ info("Going offline, creating a new card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-created");
+ let syncFailedPromise = promiseSyncFailed();
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "A New Card";
+ newCard.UID = "a-new-card";
+ newCard = directory.addCard(newCard);
+ await contactPromise;
+ await syncFailedPromise;
+
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card should have been added on client while offline"
+ );
+ Assert.ok(
+ directory.childCards.find(c => c.UID == "a-new-card"),
+ "card should have been added on client"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server before syncing"
+ );
+
+ contactPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+ let [notificationCard] = await contactPromise;
+ notificationCard.QueryInterface(Ci.nsIAbCard);
+ Assert.equal(
+ notificationCard.UID,
+ "a-new-card",
+ "correct card should have been updated"
+ );
+
+ Assert.equal(
+ notificationCard.getProperty("_href", "WRONG"),
+ `${CardDAVServer.path}a-new-card.vcf`,
+ "card should have been given _href property"
+ );
+ Assert.equal(
+ notificationCard.getProperty("_etag", "WRONG"),
+ "68",
+ "card should have been given _etag property"
+ );
+ vCardEqual(
+ notificationCard.getProperty("_vCard", "WRONG"),
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ "card should have been given _vCard property"
+ );
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "63",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I shall be changed.\r\nEND:VCARD\r\n",
+ },
+ ["delete-me"]: {
+ etag: "61",
+ href: `${CardDAVServer.path}delete-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/** Changing an existing card while "offline". */
+async function subtestUpdateCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 3, "card count on server before test");
+
+ info("Going offline, changing a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let syncFailedPromise = promiseSyncFailed();
+ let cardToChange = directory.childCards.find(c => c.UID == "change-me");
+ cardToChange.displayName = "I'm a new man!";
+ cardToChange = directory.modifyCard(cardToChange);
+ await contactPromise;
+ await syncFailedPromise;
+
+ Assert.equal(
+ directory.childCards.find(c => c.UID == "change-me").displayName,
+ "I'm a new man!",
+ "card should have been changed on client while offline"
+ );
+ Assert.stringContains(
+ CardDAVServer.cards.get(`${CardDAVServer.path}change-me.vcf`).vCard,
+ "I shall be changed.",
+ "card should NOT have been changed on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ Assert.stringContains(
+ CardDAVServer.cards.get(`${CardDAVServer.path}change-me.vcf`).vCard,
+ "I shall be changed.",
+ "card should NOT have been changed on server before syncing"
+ );
+
+ contactPromise = TestUtils.topicObserved("addrbook-contact-updated");
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+ let [notificationCard] = await contactPromise;
+ notificationCard.QueryInterface(Ci.nsIAbCard);
+ Assert.equal(
+ notificationCard.UID,
+ "change-me",
+ "correct card should have been updated"
+ );
+
+ Assert.equal(
+ notificationCard.getProperty("_href", "WRONG"),
+ `${CardDAVServer.path}change-me.vcf`,
+ "card _href property didn't change"
+ );
+ Assert.equal(
+ notificationCard.getProperty("_etag", "WRONG"),
+ "58",
+ "card _etag property did change"
+ );
+ vCardEqual(
+ notificationCard.getProperty("_vCard", "WRONG"),
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ "card _vCard property did change"
+ );
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["delete-me"]: {
+ etag: "61",
+ href: `${CardDAVServer.path}delete-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/** Deleting an existing card while "offline". */
+async function subtestDeleteCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 3, "card count on server before test");
+
+ info("Going offline, deleting a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ let syncFailedPromise = promiseSyncFailed();
+ let cardToDelete = directory.childCards.find(c => c.UID == "delete-me");
+ directory.deleteCards([cardToDelete]);
+ await contactPromise;
+ await syncFailedPromise;
+
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card should have been removed on client while offline"
+ );
+ Assert.ok(
+ !directory.childCards.find(c => c.UID == "delete-me"),
+ "card should have been removed on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 3,
+ "card should NOT have been removed on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 3,
+ "card should NOT have been removed on server before syncing"
+ );
+
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/** Adding a new card and deleting it again while "offline". */
+async function subtestCreateDeleteCard() {
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test");
+
+ info("Going offline, adding a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-created");
+ let syncFailedPromise = promiseSyncFailed();
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "A Temporary Card";
+ newCard.UID = "a-temporary-card";
+ newCard = directory.addCard(newCard);
+ await contactPromise;
+ await syncFailedPromise;
+
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card should have been added on client while offline"
+ );
+ Assert.ok(
+ directory.childCards.find(c => c.UID == "a-temporary-card"),
+ "card should have been added on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server while offline"
+ );
+
+ info("Deleting the same card before syncing.");
+ contactPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ directory.deleteCards([newCard]);
+ await contactPromise;
+ // No addrbook-directory-sync-failed notification here, we didn't attempt to
+ // delete a card that wasn't on the server (it had no _href property).
+
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card should have been removed on client while offline"
+ );
+ Assert.ok(
+ !directory.childCards.find(c => c.UID == "a-temporary-card"),
+ "card should have been removed on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been on server while offline"
+ );
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["a-new-card"]: {
+ etag: "68",
+ href: `${CardDAVServer.path}a-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+/**
+ * Check that doing a sync while offline does nothing crazy. First make both
+ * kinds of changes, then sync while offline.
+ */
+async function subtestStillOffline() {
+ Assert.equal(
+ directory.childCards.length,
+ 2,
+ "card count on client before test"
+ );
+ Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test");
+
+ info("Going offline, adding a card.");
+ await CardDAVServer.close();
+
+ let contactPromise = TestUtils.topicObserved("addrbook-contact-created");
+ let syncFailedPromise = promiseSyncFailed();
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "Another New Card";
+ newCard.UID = "another-new-card";
+ newCard = directory.addCard(newCard);
+ await contactPromise;
+ await syncFailedPromise;
+
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ Assert.equal(
+ directory.childCards.length,
+ 3,
+ "card should have been added on client while offline"
+ );
+ Assert.ok(
+ directory.childCards.find(c => c.UID == "another-new-card"),
+ "card should have been added on client while offline"
+ );
+ Assert.equal(
+ CardDAVServer.cards.size,
+ 2,
+ "card should NOT have been added on server while offline"
+ );
+
+ info("Still offline, deleting a card.");
+ let cardToDelete = directory.childCards.find(c => c.UID == "a-new-card");
+ contactPromise = TestUtils.topicObserved("addrbook-contact-deleted");
+ syncFailedPromise = promiseSyncFailed();
+ directory.deleteCards([cardToDelete]);
+ await contactPromise;
+ await syncFailedPromise;
+
+ // Ensure we've finished any async stuff.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 125));
+
+ info("Still offline, attempting to sync.");
+ syncFailedPromise = promiseSyncFailed();
+ // Assert.rejects eats the thrown exception, so we don't see it logged here.
+ await Assert.rejects(
+ directory.syncWithServer(),
+ /NS_ERROR_CONNECTION_REFUSED/,
+ "Attempt to sync threw an exception"
+ );
+ await syncFailedPromise;
+
+ await pretendToRestart();
+ syncFailedPromise = promiseSyncFailed();
+ // Assert.rejects eats the thrown exception, so we don't see it logged here.
+ await Assert.rejects(
+ directory.syncWithServer(),
+ /NS_ERROR_CONNECTION_REFUSED/,
+ "Attempt to sync threw an exception"
+ );
+ await syncFailedPromise;
+
+ info("Going online and syncing.");
+ await pretendToRestart(directory);
+ CardDAVServer.reopen();
+
+ let syncSucceededPromise = promiseSyncSucceeded();
+ await directory.syncWithServer();
+ await syncSucceededPromise;
+
+ await checkCardsOnServer({
+ ["change-me"]: {
+ etag: "58",
+ href: `${CardDAVServer.path}change-me.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n",
+ },
+ ["another-new-card"]: {
+ etag: "80",
+ href: `${CardDAVServer.path}another-new-card.vcf`,
+ vCard:
+ "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Another New Card\r\nUID:another-new-card\r\nEND:VCARD\r\n",
+ },
+ });
+}
+
+add_task(async function test_syncV1_noRestart() {
+ restart = false;
+ useSyncV1 = true;
+ await subtest();
+});
+
+add_task(async function test_syncV1_restart() {
+ restart = true;
+ useSyncV1 = true;
+ await subtest();
+});
+
+add_task(async function test_syncV2_noRestart() {
+ restart = false;
+ useSyncV1 = false;
+ await subtest();
+});
+
+add_task(async function test_syncV2_restart() {
+ restart = true;
+ useSyncV1 = false;
+ await subtest();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js
new file mode 100644
index 0000000000..244e27617e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js
@@ -0,0 +1,68 @@
+/* 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/. */
+
+/* Tests what happens if a server modifies a card when it first arrives.
+ * In this test the server changes the card's UID and path, which Google's
+ * CardDAV server does, and also adds a new property. All changes should be
+ * reflected in the client. */
+
+add_task(async () => {
+ CardDAVServer.modifyCardOnPut = true;
+
+ let directory = initDirectory();
+ await directory.fetchAllFromServer();
+
+ observer.init();
+
+ // Create a new card, and check it has the right UID.
+
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "A New Card";
+ newCard.UID = "a-new-card";
+ newCard = directory.addCard(newCard);
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["a-new-card"],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ Assert.equal(directory.childCards.length, 1);
+ Assert.equal(directory.childCards[0].UID, "a-new-card");
+
+ // Wait for notifications. Both arrive at once so we listen for the first.
+
+ let newUID = await observer.waitFor("addrbook-contact-created");
+ Assert.equal(newUID, "drac-wen-a");
+
+ // Check the original card was deleted.
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["a-new-card"],
+ });
+
+ // Check we have the card as modified by the server.
+
+ Assert.equal(directory.childCards.length, 1);
+ let modifiedCard = directory.childCards[0];
+ Assert.equal(modifiedCard.UID, "drac-wen-a");
+ Assert.equal(modifiedCard.getProperty("_etag", ""), "92");
+ Assert.equal(
+ modifiedCard.getProperty("_href", ""),
+ "/addressbooks/me/test/drac-wen-a.vcf"
+ );
+ Assert.stringContains(
+ modifiedCard.getProperty("_vCard", ""),
+ "UID:drac-wen-a\r\n"
+ );
+ Assert.stringContains(
+ modifiedCard.getProperty("_vCard", ""),
+ "X-MODIFIED-BY-SERVER:1\r\n"
+ );
+
+ await clearDirectory(directory);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js
new file mode 100644
index 0000000000..0ec5a65ae0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js
@@ -0,0 +1,282 @@
+/* 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/. */
+
+async function subtest() {
+ // Put some cards on the server.
+ CardDAVServer.putCardInternal(
+ "keep-me.vcf",
+ "BEGIN:VCARD\r\nUID:keep-me\r\nFN:I'm going to stay.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ // This one includes a character encoded with UTF-8.
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm going to be changed. \xCF\x9E\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "delete-me.vcf",
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:I'm going to be deleted.\r\nEND:VCARD\r\n"
+ );
+
+ let directory = initDirectory();
+
+ // We'll only use this for the initial sync, so I think it's okay to use
+ // bulkAddCards and not get a notification for every contact.
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ info("Cards:");
+ let cardMap = new Map();
+ let oldETags = new Map();
+ for (let card of directory.childCards) {
+ info(card.displayName);
+ info(card.getProperty("_href", ""));
+ info(card.getProperty("_etag", ""));
+
+ cardMap.set(card.UID, card);
+ oldETags.set(card.UID, card.getProperty("_etag", ""));
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), [
+ "change-me",
+ "delete-me",
+ "keep-me",
+ ]);
+ Assert.equal(
+ cardMap.get("change-me").displayName,
+ "I'm going to be changed. Ϟ"
+ );
+
+ // Make some changes on the server.
+
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I've been changed.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.deleteCardInternal("delete-me.vcf");
+ CardDAVServer.putCardInternal(
+ "new.vcf",
+ "BEGIN:VCARD\r\nUID:new\r\nFN:I'm new!\r\nEND:VCARD\r\n"
+ );
+
+ // Sync with the server.
+
+ info("Second sync with server.");
+
+ observer.init();
+ await directory.updateAllFromServerV1();
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["new"],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": ["delete-me"],
+ });
+
+ info("Cards:");
+ cardMap.clear();
+ for (let card of directory.childCards) {
+ info(card.displayName);
+ info(card.getProperty("_href", ""));
+ info(card.getProperty("_etag", ""));
+
+ cardMap.set(card.UID, card);
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), ["change-me", "keep-me", "new"]);
+
+ Assert.equal(
+ cardMap.get("keep-me").getProperty("_etag", ""),
+ oldETags.get("keep-me")
+ );
+
+ Assert.equal(cardMap.get("change-me").displayName, "I've been changed.");
+ Assert.notEqual(
+ cardMap.get("change-me").getProperty("_etag", ""),
+ oldETags.get("change-me")
+ );
+ oldETags.set("change-me", cardMap.get("change-me").getProperty("_etag", ""));
+
+ Assert.equal(cardMap.get("new").displayName, "I'm new!");
+ oldETags.set("new", cardMap.get("new").getProperty("_etag", ""));
+
+ oldETags.delete("delete-me");
+
+ // Double-check that what we have matches what's on the server.
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ new: {
+ etag: cardMap.get("new").getProperty("_etag", ""),
+ href: cardMap.get("new").getProperty("_href", ""),
+ vCard: cardMap.get("new").getProperty("_vCard", ""),
+ },
+ });
+
+ info("Third sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV1();
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ // Delete a card on the client.
+
+ info("Deleting a card on the client.");
+
+ try {
+ directory.deleteCards([cardMap.get("new")]);
+ Assert.ok(!directory.readOnly, "read-only directory should throw");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["new"],
+ });
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Change a card on the client.
+
+ info("Changing a card on the client.");
+
+ try {
+ let changeMeCard = cardMap.get("change-me");
+ changeMeCard.displayName = "I've been changed again!";
+
+ directory.modifyCard(changeMeCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw");
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "change-me"
+ );
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": [],
+ });
+
+ changeMeCard = directory.childCards.find(c => c.UID == "change-me");
+ cardMap.set("change-me", changeMeCard);
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: changeMeCard.getProperty("_etag", ""),
+ href: changeMeCard.getProperty("_href", ""),
+ vCard: changeMeCard.getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Add a new card on the client.
+
+ info("Adding a new card on the client.");
+
+ try {
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "I'm another new contact. ϔ";
+ newCard.UID = "another-new";
+ newCard = directory.addCard(newCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["another-new"],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "another-new"
+ );
+
+ newCard = directory.childCards.find(c => c.UID == "another-new");
+ Assert.equal(
+ newCard.displayName,
+ "I'm another new contact. ϔ",
+ "non-ascii character survived the trip to the server"
+ );
+
+ await checkCardsOnServer({
+ "another-new": {
+ etag: newCard.getProperty("_etag", ""),
+ href: newCard.getProperty("_href", ""),
+ vCard: newCard.getProperty("_vCard", ""),
+ },
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ info("Fourth sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV1();
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+}
+
+add_task(async function testNormal() {
+ await subtest();
+});
+
+add_task(async function testYahoo() {
+ CardDAVServer.mimicYahoo = true;
+ await subtest();
+ CardDAVServer.mimicYahoo = false;
+});
+
+add_task(async function testReadOnly() {
+ Services.prefs.setBoolPref("ldap_2.servers.carddav.readOnly", true);
+ await subtest();
+ Services.prefs.clearUserPref("ldap_2.servers.carddav.readOnly");
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js
new file mode 100644
index 0000000000..74f9c5ac88
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js
@@ -0,0 +1,408 @@
+/* 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/. */
+
+async function subtest() {
+ // Put some cards on the server.
+ CardDAVServer.putCardInternal(
+ "keep-me.vcf",
+ "BEGIN:VCARD\r\nUID:keep-me\r\nFN:I'm going to stay.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ // This one includes a character encoded with UTF-8.
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm going to be changed. \xCF\x9E\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "delete-me.vcf",
+ "BEGIN:VCARD\r\nUID:delete-me\r\nFN:I'm going to be deleted.\r\nEND:VCARD\r\n"
+ );
+
+ let directory = initDirectory();
+
+ // We'll only use this for the initial sync, so I think it's okay to use
+ // bulkAddCards and not get a notification for every contact.
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ let lastSyncToken = directory._syncToken;
+ info(`Token is: ${lastSyncToken}`);
+
+ info("Cards:");
+ let cardMap = new Map();
+ let oldETags = new Map();
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+
+ cardMap.set(card.UID, card);
+ oldETags.set(card.UID, card.getProperty("_etag", ""));
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), [
+ "change-me",
+ "delete-me",
+ "keep-me",
+ ]);
+ Assert.equal(
+ cardMap.get("change-me").displayName,
+ "I'm going to be changed. Ϟ"
+ );
+
+ // Make some changes on the server.
+
+ CardDAVServer.putCardInternal(
+ "change-me.vcf",
+ "BEGIN:VCARD\r\nUID:change-me\r\nFN:I've been changed.\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.deleteCardInternal("delete-me.vcf");
+ CardDAVServer.putCardInternal(
+ "new.vcf",
+ "BEGIN:VCARD\r\nUID:new\r\nFN:I'm new!\r\nEND:VCARD\r\n"
+ );
+
+ // Sync with the server.
+
+ info("Second sync with server.");
+
+ observer.init();
+ await directory.updateAllFromServerV2();
+ Assert.notEqual(directory._syncToken, lastSyncToken);
+ lastSyncToken = directory._syncToken;
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["new"],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": ["delete-me"],
+ });
+
+ info("Cards:");
+ cardMap.clear();
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+
+ cardMap.set(card.UID, card);
+ }
+
+ Assert.equal(cardMap.size, 3);
+ Assert.deepEqual([...cardMap.keys()].sort(), ["change-me", "keep-me", "new"]);
+
+ Assert.equal(
+ cardMap.get("keep-me").getProperty("_etag", ""),
+ oldETags.get("keep-me")
+ );
+
+ Assert.equal(cardMap.get("change-me").displayName, "I've been changed.");
+ Assert.notEqual(
+ cardMap.get("change-me").getProperty("_etag", ""),
+ oldETags.get("change-me")
+ );
+ oldETags.set("change-me", cardMap.get("change-me").getProperty("_etag", ""));
+
+ Assert.equal(cardMap.get("new").displayName, "I'm new!");
+ oldETags.set("new", cardMap.get("new").getProperty("_etag", ""));
+
+ oldETags.delete("delete-me");
+
+ // Double-check that what we have matches what's on the server.
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ new: {
+ etag: cardMap.get("new").getProperty("_etag", ""),
+ href: cardMap.get("new").getProperty("_href", ""),
+ vCard: cardMap.get("new").getProperty("_vCard", ""),
+ },
+ });
+
+ info("Third sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV2();
+ // This time the token should NOT change, there's been no contact with the
+ // server since last time.
+ Assert.equal(directory._syncToken, lastSyncToken);
+ lastSyncToken = directory._syncToken;
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ // Delete a card on the client.
+
+ info("Deleting a card on the client.");
+
+ try {
+ directory.deleteCards([cardMap.get("new")]);
+ Assert.ok(!directory.readOnly, "read-only directory should throw.");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["new"],
+ });
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Change a card on the client.
+
+ info("Changing a card on the client.");
+
+ try {
+ let changeMeCard = cardMap.get("change-me");
+ changeMeCard.displayName = "I've been changed again!";
+ directory.modifyCard(changeMeCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw.");
+
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "change-me"
+ );
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": ["change-me"],
+ "addrbook-contact-deleted": [],
+ });
+
+ changeMeCard = directory.childCards.find(c => c.UID == "change-me");
+ cardMap.set("change-me", changeMeCard);
+
+ await checkCardsOnServer({
+ "change-me": {
+ etag: changeMeCard.getProperty("_etag", ""),
+ href: changeMeCard.getProperty("_href", ""),
+ vCard: changeMeCard.getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ // Add a new card on the client.
+
+ info("Adding a new card on the client.");
+
+ try {
+ let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ newCard.displayName = "I'm another new contact. ϔ";
+ newCard.UID = "another-new";
+ newCard = directory.addCard(newCard);
+ Assert.ok(!directory.readOnly, "read-only directory should throw.");
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": ["another-new"],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ Assert.equal(
+ await observer.waitFor("addrbook-contact-updated"),
+ "another-new"
+ );
+
+ newCard = directory.childCards.find(c => c.UID == "another-new");
+ Assert.equal(
+ newCard.displayName,
+ "I'm another new contact. ϔ",
+ "non-ascii character survived the trip to the server"
+ );
+
+ await checkCardsOnServer({
+ "another-new": {
+ etag: newCard.getProperty("_etag", ""),
+ href: newCard.getProperty("_href", ""),
+ vCard: newCard.getProperty("_vCard", ""),
+ },
+ "change-me": {
+ etag: cardMap.get("change-me").getProperty("_etag", ""),
+ href: cardMap.get("change-me").getProperty("_href", ""),
+ vCard: cardMap.get("change-me").getProperty("_vCard", ""),
+ },
+ "keep-me": {
+ etag: cardMap.get("keep-me").getProperty("_etag", ""),
+ href: cardMap.get("keep-me").getProperty("_href", ""),
+ vCard: cardMap.get("keep-me").getProperty("_vCard", ""),
+ },
+ });
+ } catch (ex) {
+ Assert.ok(directory.readOnly, "read-write directory should not throw");
+ }
+
+ info("Fourth sync with server. No changes expected.");
+
+ await directory.updateAllFromServerV2();
+ if (directory.readOnly) {
+ Assert.equal(directory._syncToken, lastSyncToken);
+ } else {
+ Assert.notEqual(directory._syncToken, lastSyncToken);
+ }
+
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": [],
+ });
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+}
+
+add_task(async function testNormal() {
+ await subtest();
+});
+
+add_task(async function testGoogle() {
+ CardDAVServer.mimicGoogle = true;
+ Services.prefs.setBoolPref("ldap_2.servers.carddav.carddav.vcard3", true);
+ await subtest();
+ Services.prefs.clearUserPref("ldap_2.servers.carddav.carddav.vcard3");
+ CardDAVServer.mimicGoogle = false;
+});
+
+add_task(async function testReadOnly() {
+ Services.prefs.setBoolPref("ldap_2.servers.carddav.readOnly", true);
+ await subtest();
+ Services.prefs.clearUserPref("ldap_2.servers.carddav.readOnly");
+});
+
+add_task(async function testExpiredToken() {
+ // Put some cards on the server.
+ CardDAVServer.putCardInternal(
+ "first.vcf",
+ "BEGIN:VCARD\r\nUID:first\r\nFN:First Person\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "second.vcf",
+ "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "third.vcf",
+ "BEGIN:VCARD\r\nUID:third\r\nFN:Third Person\r\nEND:VCARD\r\n"
+ );
+
+ let directory = initDirectory();
+
+ info("Initial sync with server.");
+ await directory.fetchAllFromServer();
+
+ info(`Token is: ${directory._syncToken}`);
+
+ info("Cards:");
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+ }
+
+ Assert.equal(directory.childCardCount, 3);
+ Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [
+ "first",
+ "second",
+ "third",
+ ]);
+
+ // Corrupt the sync token. This will cause a 400 Bad Request response and a
+ // complete resync should happen.
+
+ directory._syncToken = "wrong token";
+
+ // Make some changes on the server.
+
+ CardDAVServer.putCardInternal(
+ "fourth.vcf",
+ "BEGIN:VCARD\r\nUID:fourth\r\nFN:Fourth\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.putCardInternal(
+ "second.vcf",
+ "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person, but different\r\nEND:VCARD\r\n"
+ );
+ CardDAVServer.deleteCardInternal("first.vcf");
+
+ // Sync with the server.
+
+ info("Sync with server.");
+
+ let notificationPromise = TestUtils.topicObserved(
+ "addrbook-directory-invalidated"
+ );
+ observer.init();
+ await directory.updateAllFromServerV2();
+ // Check what notifications were fired. There should be an "invalidated"
+ // notification, making the others redundant, but the "deleted"
+ // notification is hard to avoid.
+ observer.checkAndClearNotifications({
+ "addrbook-contact-created": [],
+ "addrbook-contact-updated": [],
+ "addrbook-contact-deleted": ["first"],
+ });
+ await notificationPromise;
+
+ info(`Token is now: ${directory._syncToken}`);
+
+ info("Cards:");
+ for (let card of directory.childCards) {
+ info(
+ ` ${card.displayName} [${card.getProperty(
+ "_href",
+ ""
+ )}, ${card.getProperty("_etag", "")}]`
+ );
+ }
+
+ // Check that the changes were synced.
+
+ Assert.equal(directory.childCardCount, 3);
+ Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [
+ "fourth",
+ "second",
+ "third",
+ ]);
+ Assert.equal(
+ directory.childCards.find(c => c.UID == "second").displayName,
+ "Second Person, but different"
+ );
+
+ await clearDirectory(directory);
+ CardDAVServer.reset();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_cardForEmail.js b/comm/mailnews/addrbook/test/unit/test_cardForEmail.js
new file mode 100644
index 0000000000..3e7a53f339
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_cardForEmail.js
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests nsIAbDirectory::cardForEmailAddress
+ * - checks correct return when no email address supplied
+ * - checks correct return when no matching email address supplied
+ * - checks correct return when matching email address supplied.
+ *
+ * Uses: cardForEmail.mab
+ */
+
+function check_correct_card(card) {
+ Assert.ok(!!card);
+
+ Assert.equal(card.firstName, "FirstName1");
+ Assert.equal(card.lastName, "LastName1");
+ Assert.equal(card.displayName, "DisplayName1");
+ Assert.deepEqual(card.emailAddresses, [
+ "PrimaryEmail1@test.invalid",
+ "SecondEmail1\u00D0@test.invalid",
+ ]);
+}
+
+function run_test() {
+ loadABFile("data/cardForEmail", kPABData.fileName);
+
+ // Test - Get the directory
+ let AB = MailServices.ab.getDirectory(kPABData.URI);
+
+ // Test - Check that a null string succeeds and does not
+ // return a card (bug 404264)
+ Assert.ok(AB.cardForEmailAddress(null) == null);
+
+ // Test - Check that an empty string succeeds and does not
+ // return a card (bug 404264)
+ Assert.ok(AB.cardForEmailAddress("") == null);
+
+ // Test - Check that we don't match an email that doesn't exist
+ Assert.ok(AB.cardForEmailAddress("nocard@this.email.invalid") == null);
+
+ // Test - Check that we match this email and some of the fields
+ // of the card are correct.
+ var card = AB.cardForEmailAddress("PrimaryEmail1@test.invalid");
+
+ check_correct_card(card);
+
+ // Test - Check that we match with the primary email with insensitive case.
+ card = AB.cardForEmailAddress("pRimaryemAIL1@teST.invalid");
+
+ check_correct_card(card);
+
+ // Test - Check that we match with the second email.
+ card = AB.cardForEmailAddress("SecondEmail1\u00D0@test.invalid");
+
+ check_correct_card(card);
+
+ // Test - Check that we match with the second email with insensitive case.
+ card = AB.cardForEmailAddress("SECondEMail1\u00D0@TEST.inValid");
+
+ check_correct_card(card);
+
+ // Check that we match cards that have more than two email addresses.
+ card = AB.cardForEmailAddress("first@SOMETHING.invalid");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("second@something.INVALID");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("THIRD@something.invalid");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("FOURTH@SOMETHING.INVALID");
+ Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1");
+ Assert.equal(card.displayName, "A vCard!");
+
+ card = AB.cardForEmailAddress("A vCard!");
+ Assert.equal(card, null);
+
+ // Check getCardFromProperty returns null correctly for non-extant properties
+ Assert.equal(AB.getCardFromProperty("NickName", "", false), null);
+ Assert.equal(AB.getCardFromProperty("NickName", "NickName", false), null);
+
+ // Check case-insensitive searching works
+ card = AB.getCardFromProperty("NickName", "NickName1", true);
+ check_correct_card(card);
+ card = AB.getCardFromProperty("NickName", "NickName1", false);
+ check_correct_card(card);
+
+ Assert.equal(AB.getCardFromProperty("NickName", "nickName1", true), null);
+
+ card = AB.getCardFromProperty("NickName", "nickName1", false);
+ check_correct_card(card);
+
+ var cards = AB.getCardsFromProperty("LastName", "DOE", true);
+ Assert.equal(cards.length, 0);
+
+ cards = AB.getCardsFromProperty("LastName", "Doe", true);
+ var i = 0;
+ var data = ["John", "Jane"];
+
+ for (card of cards) {
+ i++;
+ Assert.equal(card.lastName, "Doe");
+ var index = data.indexOf(card.firstName);
+ Assert.notEqual(index, -1);
+ delete data[index];
+ }
+ Assert.equal(i, 2);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_collection.js b/comm/mailnews/addrbook/test/unit/test_collection.js
new file mode 100644
index 0000000000..720f28c246
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_collection.js
@@ -0,0 +1,404 @@
+/* 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/. */
+
+/*
+ * Test suite for the Address Collector Service.
+ *
+ * This tests the main collection functions for adding new cards and modifying
+ * existing ones.
+ *
+ * Tests against cards in different ABs are done in test_collection_2.js.
+ */
+
+// Source fields (emailHeader) and expected results for use for
+// testing the addition of new addresses to the database.
+//
+// Note: these email addresses should be different to allow collecting an
+// address to add a different card each time.
+var addEmailChecks =
+ // First 3 items aimed at basic collection and mail format.
+ [
+ {
+ emailHeader: "test0@foo.invalid",
+ primaryEmail: "test0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "test1@foo.invalid",
+ primaryEmail: "test1@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "test2@foo.invalid",
+ primaryEmail: "test2@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // UTF-8 based addresses (bug 407564)
+ {
+ emailHeader: "test0@\u00D0.invalid",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "test0\u00D0@foo.invalid",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // Screen names
+ {
+ emailHeader: "invalid\u00D00@aol.com",
+ primaryEmail: "invalid\u00D00@aol.com",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "invalid\u00D00",
+ },
+ {
+ emailHeader: "invalid1\u00D00@cs.com",
+ primaryEmail: "invalid1\u00D00@cs.com",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "invalid1\u00D00",
+ },
+ {
+ emailHeader: "invalid2\u00D00@netscape.net",
+ primaryEmail: "invalid2\u00D00@netscape.net",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "invalid2\u00D00",
+ },
+ // Collection of names
+ {
+ emailHeader: "Test User <test3@foo.invalid>",
+ primaryEmail: "test3@foo.invalid",
+ displayName: "Test User",
+ firstName: "Test",
+ lastName: "User",
+ screenName: "",
+ },
+ {
+ emailHeader: "Test <test4@foo.invalid>",
+ primaryEmail: "test4@foo.invalid",
+ displayName: "Test",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // Collection of names with UTF-8 specific items
+ {
+ emailHeader: "Test\u00D0 User <test5@foo.invalid>",
+ primaryEmail: "test5@foo.invalid",
+ displayName: "Test\u00D0 User",
+ firstName: "Test\u00D0",
+ lastName: "User",
+ screenName: "",
+ },
+ {
+ emailHeader: "Test\u00D0 <test6@foo.invalid>",
+ primaryEmail: "test6@foo.invalid",
+ displayName: "Test\u00D0",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ ];
+
+// Source fields (emailHeader) and expected results for use for
+// testing the modification of cards in the database.
+//
+// Note: these sets re-use some of the ones for ease of definition.
+var modifyEmailChecks =
+ // No display name/other details. Add details and modify mail format.
+ [
+ {
+ emailHeader: "Modify User\u00D0 <test0@\u00D0.invalid>",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "Modify User\u00D0",
+ firstName: "Modify",
+ lastName: "User\u00D0",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify <test0\u00D0@foo.invalid>",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "Modify",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // No modification of existing cards with display names
+ {
+ emailHeader: "Modify2 User\u00D02 <test0@\u00D0.invalid>",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "Modify User\u00D0",
+ firstName: "Modify",
+ lastName: "User\u00D0",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify3 <test0\u00D0@foo.invalid>",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "Modify",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // Check no modification of cards for mail format where format is not
+ // "unknown".
+ {
+ emailHeader: "Modify User\u00D0 <test0@\u00D0.invalid>",
+ primaryEmail: "test0@\u00D0.invalid",
+ displayName: "Modify User\u00D0",
+ firstName: "Modify",
+ lastName: "User\u00D0",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify <test0\u00D0@foo.invalid>",
+ primaryEmail: "test0\u00D0@foo.invalid",
+ displayName: "Modify",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ // No modification of cards with email in second email address.
+ {
+ emailHeader: "Modify Secondary <usersec\u00D0@foo.invalid>",
+ primaryEmail: "userprim\u00D0@foo.invalid",
+ secondEmail: "usersec\u00D0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ {
+ emailHeader: "Modify <usersec\u00D0@foo.invalid>",
+ primaryEmail: "userprim\u00D0@foo.invalid",
+ secondEmail: "usersec\u00D0@foo.invalid",
+ displayName: "",
+ firstName: "",
+ lastName: "",
+ screenName: "",
+ },
+ ];
+
+var collectChecker = {
+ addressCollect: null,
+ AB: null,
+ part: 0,
+
+ checkAddress(aDetails) {
+ info("checkAddress: " + aDetails.emailHeader);
+ try {
+ this.addressCollect.collectAddress(aDetails.emailHeader, true);
+
+ this.checkCardResult(aDetails, false);
+ } catch (e) {
+ throw new Error(
+ "FAILED in checkAddress emailHeader: " +
+ aDetails.emailHeader +
+ " part: " +
+ this.part +
+ " : " +
+ e
+ );
+ }
+ ++this.part;
+ },
+
+ checkAll(aDetailsArray) {
+ try {
+ // Formulate the string to add.
+ var emailHeader = "";
+ var i;
+
+ for (i = 0; i < aDetailsArray.length - 1; ++i) {
+ emailHeader += aDetailsArray[i].emailHeader + ", ";
+ }
+
+ emailHeader += aDetailsArray[aDetailsArray.length - 1].emailHeader;
+
+ // Now add it. In this case we just set the Mail format Type to unknown.
+ this.addressCollect.collectAddress(emailHeader, true);
+
+ for (i = 0; i < aDetailsArray.length; ++i) {
+ this.checkCardResult(aDetailsArray[i], true);
+ }
+ } catch (e) {
+ throw new Error("FAILED in checkAll item: " + i + " : " + e);
+ }
+ },
+
+ checkCardResult(aDetails) {
+ info("checkCardResult: " + aDetails.emailHeader);
+ try {
+ var card = this.AB.cardForEmailAddress(aDetails.primaryEmail);
+
+ Assert.ok(card != null);
+
+ if ("secondEmail" in aDetails) {
+ Assert.equal(card.emailAddresses[1], aDetails.secondEmail);
+ }
+
+ Assert.equal(card.displayName, aDetails.displayName);
+ Assert.equal(card.firstName, aDetails.firstName);
+ Assert.equal(card.lastName, aDetails.lastName);
+ Assert.equal(card.getProperty("_AimScreenName", ""), aDetails.screenName);
+ } catch (e) {
+ throw new Error(
+ "FAILED in checkCardResult emailHeader: " +
+ aDetails.emailHeader +
+ " : " +
+ e
+ );
+ }
+ },
+};
+
+function run_test() {
+ // Test - Get the address collecter
+
+ // XXX Getting all directories ensures we create all ABs because the
+ // address collecter can't currently create ABs itself (bug 314448).
+ MailServices.ab.directories;
+
+ // Get the actual AB for the collector so we can check cards have been
+ // added.
+ collectChecker.AB = MailServices.ab.getDirectory(
+ Services.prefs.getCharPref("mail.collect_addressbook")
+ );
+
+ // Get the actual collecter
+ collectChecker.addressCollect = Cc[
+ "@mozilla.org/addressbook/services/addressCollector;1"
+ ].getService(Ci.nsIAbAddressCollector);
+
+ // Test - Addition of header without email address.
+
+ collectChecker.addressCollect.collectAddress("MyTest <>", true);
+
+ // Address book should have no cards present.
+ Assert.equal(collectChecker.AB.childCards.length, 0);
+
+ // Test - Email doesn't exist, but don't add it.
+
+ // As we've just set everything up, we know we haven't got anything in the
+ // AB, so just try and collect without adding.
+ collectChecker.addressCollect.collectAddress(
+ addEmailChecks[0].emailHeader,
+ false
+ );
+
+ var card = collectChecker.AB.cardForEmailAddress(
+ addEmailChecks[0].emailHeader
+ );
+
+ Assert.ok(card == null);
+
+ // Test - Try and collect various emails and formats.
+
+ collectChecker.part = 0;
+
+ addEmailChecks.forEach(collectChecker.checkAddress, collectChecker);
+
+ // Test - Do all emails at the same time.
+
+ // First delete all existing cards
+ collectChecker.AB.deleteCards(collectChecker.AB.childCards);
+
+ // Address book should have no cards present.
+ Assert.equal(collectChecker.AB.childCards.length, 0);
+
+ Assert.equal(
+ collectChecker.AB.cardForEmailAddress(addEmailChecks[0].emailHeader),
+ null
+ );
+
+ // Now do all emails at the same time.
+ collectChecker.checkAll(addEmailChecks);
+
+ // Test - Try and modify various emails and formats.
+
+ // Add a basic card with just primary and second email to allow testing
+ // of the case where we don't modify when second email is matching.
+ card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = "userprim\u00D0@foo.invalid";
+ card.setProperty("SecondEmail", "usersec\u00D0@foo.invalid");
+
+ collectChecker.AB.addCard(card);
+
+ collectChecker.part = 0;
+
+ modifyEmailChecks.forEach(collectChecker.checkAddress, collectChecker);
+
+ // Test collectSingleAddress - Note: because the above tests test
+ // collectAddress which we know calls collectSingleAddress, we only need to
+ // test the case where aSkipCheckExisting is true.
+
+ // Add an email that is already there and check we get two instances of it in
+ // the AB.
+
+ const kSingleAddress =
+ modifyEmailChecks[modifyEmailChecks.length - 1].primaryEmail;
+ const kSingleDisplayName = "Test Single";
+
+ collectChecker.addressCollect.collectSingleAddress(
+ kSingleAddress,
+ kSingleDisplayName,
+ true,
+ true
+ );
+
+ // Try collecting the same address in another case. This shouldn't create any
+ // new card.
+ collectChecker.addressCollect.collectSingleAddress(
+ kSingleAddress.toUpperCase(),
+ kSingleDisplayName,
+ true,
+ true
+ );
+
+ var foundCards = [];
+
+ for (card of collectChecker.AB.childCards) {
+ if (card.primaryEmail == kSingleAddress) {
+ foundCards.push(card);
+ }
+ }
+
+ Assert.equal(foundCards.length, 2);
+
+ if (
+ foundCards[0].displayName != kSingleDisplayName &&
+ foundCards[1].displayName != kSingleDisplayName
+ ) {
+ do_throw("Error, collectSingleCard didn't create a new card");
+ }
+
+ if (foundCards[0].displayName != "" && foundCards[1].displayName != "") {
+ do_throw(
+ "Error, collectSingleCard created ok, but other card does not exist"
+ );
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_collection_2.js b/comm/mailnews/addrbook/test/unit/test_collection_2.js
new file mode 100644
index 0000000000..bff3d6e916
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_collection_2.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/*
+ * Test suite for the Address Collector Service part 2.
+ *
+ * This test checks that we don't collect addresses when they already exist
+ * in other address books.
+ */
+
+function run_test() {
+ // Test - Get the address collector
+ loadABFile("data/collect", kPABData.fileName);
+
+ // Get the actual collector
+ var addressCollect = Cc[
+ "@mozilla.org/addressbook/services/addressCollector;1"
+ ].getService(Ci.nsIAbAddressCollector);
+
+ // Set the new pref afterwards to ensure we change correctly
+ Services.prefs.setCharPref("mail.collect_addressbook", kCABData.URI);
+
+ // XXX Getting all directories ensures we create all ABs because the
+ // address collector can't currently create ABs itself (bug 314448).
+ MailServices.ab.directories;
+
+ addressCollect.collectAddress("Other Book <other@book.invalid>", true);
+
+ let PAB = MailServices.ab.getDirectory(kPABData.URI);
+
+ var cards = PAB.childCards;
+
+ Assert.equal(cards.length, 1);
+
+ Assert.equal(cards[0].displayName, "Other Book");
+ Assert.equal(cards[0].primaryEmail, "other@book.invalid");
+
+ // Check the CAB has no cards.
+ let CAB = MailServices.ab.getDirectory(kCABData.URI);
+ Assert.equal(CAB.childCards.length, 0);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_convertOnSave.js b/comm/mailnews/addrbook/test/unit/test_convertOnSave.js
new file mode 100644
index 0000000000..da26ffe56c
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_convertOnSave.js
@@ -0,0 +1,329 @@
+/* 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/. */
+
+// Tests that any card added to an AddrBookDirectory is stored as a vCard.
+// Some properties are also recorded outside the vCard for performance reasons
+// and/or searching.
+
+// Each type of card is saved and checked twice: once with its own UID and
+// again with a new UID. This ensures that UIDs are appropriately stored.
+
+var { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+var { SQLiteDirectory } = ChromeUtils.import(
+ "resource:///modules/SQLiteDirectory.jsm"
+);
+var { VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+Services.prefs.setStringPref(
+ "ldap_2.servers.conversion.filename",
+ "conversion.sqlite"
+);
+
+var book = new SQLiteDirectory();
+book.init("jsaddrbook://conversion.sqlite");
+
+/** Tests an instance of nsAbCardProperty. */
+add_task(function testCardProperty() {
+ let cardProperty = Cc[
+ "@mozilla.org/addressbook/cardproperty;1"
+ ].createInstance(Ci.nsIAbCard);
+ cardProperty.UID = "99999999-8888-7777-6666-555555555555";
+ cardProperty.displayName = "display name";
+ cardProperty.firstName = "first";
+ cardProperty.lastName = "last";
+ cardProperty.primaryEmail = "primary@email";
+ cardProperty.setProperty("SecondEmail", "second@email");
+ cardProperty.setProperty("NickName", "nick");
+ cardProperty.setProperty("FaxNumber", "1234567");
+ cardProperty.setProperty("BirthYear", 2001);
+ cardProperty.setProperty("BirthMonth", 1);
+ cardProperty.setProperty("BirthDay", 1);
+ cardProperty.setProperty("FakeProperty", "fake value");
+
+ saveCardAndTest(cardProperty, false);
+ saveCardAndTest(cardProperty, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *doesn't* contain a vCard, and
+ * is therefore the same as a card that hasn't yet been migrated to vCard.
+ */
+add_task(function testABCard() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["PopularityIndex", 0], // NO
+ ["DisplayName", "display name"],
+ ["FirstName", "first"],
+ ["LastName", "last"],
+ ["PrimaryEmail", "primary@email"],
+ ["SecondEmail", "second@email"],
+ ["NickName", "nick"],
+ ["FaxNumber", "1234567"],
+ ["BirthYear", 2001],
+ ["BirthMonth", 1],
+ ["BirthDay", 1],
+ ["FakeProperty", "fake value"],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *does* contain a vCard.
+ */
+add_task(function testABCardWithVCard() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["PopularityIndex", 0], // NO
+ ["DisplayName", "display name"],
+ ["FirstName", "first"],
+ ["LastName", "last"],
+ ["PrimaryEmail", "primary@email"],
+ ["SecondEmail", "second@email"],
+ ["NickName", "nick"],
+ ["FakeProperty", "fake value"],
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:primary@email
+ EMAIL:second@email
+ FN:display name
+ NICKNAME:nick
+ BDAY;VALUE=DATE:20010101
+ N:last;first;;;
+ TEL;TYPE=fax;VALUE=TEXT:1234567
+ UID:99999999-8888-7777-6666-555555555555
+ END:VCARD
+ `,
+ ],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *does* contain a vCard.
+ */
+add_task(function testABCardWithVCardOnly() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["FakeProperty", "fake value"], // NO
+ ["PopularityIndex", 0], // NO
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:primary@email
+ EMAIL:second@email
+ FN:display name
+ NICKNAME:nick
+ BDAY;VALUE=DATE:20010101
+ N:last;first;;;
+ TEL;TYPE=fax;VALUE=TEXT:1234567
+ UID:99999999-8888-7777-6666-555555555555
+ END:VCARD
+ `,
+ ],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/**
+ * Tests an instance of AddrBookCard, populated in the same way that card are
+ * created from storage. This instance *does* contain a vCard, but also some
+ * properties that shouldn't exist because their value is stored in the vCard.
+ */
+add_task(function testABCardWithVCardAndExtraProps() {
+ let abCard = new AddrBookCard();
+ abCard._uid = "99999999-8888-7777-6666-555555555555";
+ abCard._properties = new Map([
+ ["PopularityIndex", 0], // NO
+ ["DisplayName", "display name"],
+ ["FirstName", "first"],
+ ["LastName", "last"],
+ ["PrimaryEmail", "primary@email"],
+ ["SecondEmail", "second@email"],
+ ["NickName", "nick"],
+ ["FaxNumber", "1234567"],
+ ["BirthYear", 2001],
+ ["BirthMonth", 1],
+ ["BirthDay", 1],
+ ["FakeProperty", "fake value"],
+ [
+ "_vCard",
+ formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ EMAIL;PREF=1:primary@email
+ EMAIL:second@email
+ FN:display name
+ NICKNAME:nick
+ BDAY;VALUE=DATE:20010101
+ N:last;first;;;
+ TEL;TYPE=fax;VALUE=TEXT:1234567
+ UID:99999999-8888-7777-6666-555555555555
+ END:VCARD
+ `,
+ ],
+ ]);
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/** Tests an instance of AddrBookCard, created from scratch. */
+add_task(function testABCardConstructed() {
+ let abCard = new AddrBookCard();
+ abCard.UID = "99999999-8888-7777-6666-555555555555";
+ abCard.displayName = "display name";
+ abCard.firstName = "first";
+ abCard.lastName = "last";
+ abCard.primaryEmail = "primary@email";
+ abCard.vCardProperties.addValue("email", "second@email");
+ abCard.vCardProperties.addValue("nickname", "nick");
+ abCard.vCardProperties.addEntry(
+ new VCardPropertyEntry("tel", { type: "fax" }, "text", "1234567")
+ );
+ abCard.vCardProperties.addEntry(
+ new VCardPropertyEntry("bday", {}, "date", "20010101")
+ );
+ abCard.setProperty("FakeProperty", "fake value");
+
+ saveCardAndTest(abCard, false);
+ saveCardAndTest(abCard, true);
+});
+
+/** Tests an instance of AddrBookCard, created from scratch. */
+add_task(function testABCardConstructionThrows() {
+ let abCard = new AddrBookCard();
+ abCard.UID = "99999999-8888-7777-6666-555555555555";
+ abCard.displayName = "display name";
+ abCard.firstName = "first";
+ abCard.lastName = "last";
+ abCard.primaryEmail = "primary@email";
+ // these properties will be forgotten
+ Assert.throws(
+ () => abCard.setProperty("SecondEmail", "second@email"),
+ /Unable to set SecondEmail as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("NickName", "nick"),
+ /Unable to set NickName as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("FaxNumber", "1234567"),
+ /Unable to set FaxNumber as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("BirthYear", 2001),
+ /Unable to set BirthYear as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("BirthMonth", 1),
+ /Unable to set BirthMonth as a property/
+ );
+ Assert.throws(
+ () => abCard.setProperty("BirthDay", 1),
+ /Unable to set BirthDay as a property/
+ );
+ abCard.setProperty("FakeProperty", "fake value");
+});
+
+function saveCardAndTest(card, useNewUID) {
+ info(`Saving the card ${useNewUID ? "with" : "without"} a new UID`);
+
+ Assert.equal(book.childCardCount, 0);
+
+ let savedCard = book.dropCard(card, useNewUID);
+ Assert.deepEqual(Array.from(savedCard.properties, p => p.name).sort(), [
+ "DisplayName",
+ "FakeProperty",
+ "FirstName",
+ "LastModifiedDate",
+ "LastName",
+ "NickName",
+ "PopularityIndex",
+ "PrimaryEmail",
+ "SecondEmail",
+ "_vCard",
+ ]);
+
+ if (useNewUID) {
+ Assert.notEqual(savedCard.UID, "99999999-8888-7777-6666-555555555555");
+ } else {
+ Assert.equal(savedCard.UID, "99999999-8888-7777-6666-555555555555");
+ }
+
+ Assert.equal(savedCard.getProperty("DisplayName", "WRONG"), "display name");
+ Assert.equal(savedCard.getProperty("FirstName", "WRONG"), "first");
+ Assert.equal(savedCard.getProperty("LastName", "WRONG"), "last");
+ Assert.equal(savedCard.getProperty("PrimaryEmail", "WRONG"), "primary@email");
+ Assert.equal(savedCard.getProperty("SecondEmail", "WRONG"), "second@email");
+ Assert.equal(savedCard.getProperty("NickName", "WRONG"), "nick");
+ Assert.equal(savedCard.getProperty("FakeProperty", "WRONG"), "fake value");
+ Assert.equal(savedCard.getProperty("PopularityIndex", "WRONG"), "0");
+
+ let vCard = savedCard.getProperty("_vCard", "WRONG");
+ Assert.stringContains(vCard, "\r\nEMAIL;PREF=1:primary@email\r\n");
+ Assert.stringContains(vCard, "\r\nEMAIL:second@email\r\n");
+ Assert.stringContains(vCard, "\r\nFN:display name\r\n");
+ Assert.stringContains(vCard, "\r\nNICKNAME:nick\r\n");
+ Assert.stringContains(vCard, "\r\nBDAY;VALUE=DATE:20010101\r\n");
+ Assert.stringContains(vCard, "\r\nN:last;first;;;\r\n");
+ Assert.stringContains(vCard, "\r\nTEL;TYPE=fax;VALUE=TEXT:1234567\r\n");
+ Assert.stringContains(vCard, `\r\nUID:${savedCard.UID}\r\n`);
+
+ let modifiedDate = parseInt(
+ savedCard.getProperty("LastModifiedDate", ""),
+ 10
+ );
+ Assert.lessOrEqual(modifiedDate, Date.now() / 1000);
+ Assert.greater(modifiedDate, Date.now() / 1000 - 10);
+
+ Assert.equal(savedCard.displayName, "display name");
+ Assert.equal(savedCard.firstName, "first");
+ Assert.equal(savedCard.lastName, "last");
+ Assert.equal(savedCard.primaryEmail, "primary@email");
+ Assert.deepEqual(savedCard.emailAddresses, ["primary@email", "second@email"]);
+
+ Assert.ok(savedCard.supportsVCard);
+ Assert.ok(savedCard.vCardProperties);
+
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("fn"), [
+ "display name",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("email"), [
+ "primary@email",
+ "second@email",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("nickname"), [
+ "nick",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("bday"), [
+ "2001-01-01",
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("n"), [
+ ["last", "first", "", "", ""],
+ ]);
+ Assert.deepEqual(savedCard.vCardProperties.getAllValues("tel"), ["1234567"]);
+
+ book.deleteCards(book.childCards);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_db_enumerator.js b/comm/mailnews/addrbook/test/unit/test_db_enumerator.js
new file mode 100644
index 0000000000..50fe8b7d06
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_db_enumerator.js
@@ -0,0 +1,89 @@
+/**
+ * This test verifies that we don't crash if we have an enumerator on an
+ * addr database and delete the underlying directory, which forces the ab
+ * closed.
+ */
+var ab_prefix = "test-537815-";
+var card_properties = {
+ FirstName: "01-first-3",
+ LastName: "02-last",
+ PrimaryEmail: "08-email-1@zindus.invalid",
+};
+var max_addressbooks = 10;
+
+function bug_537815_fixture_setup() {
+ let i, key;
+
+ for (i = 1; i <= max_addressbooks; i++) {
+ let ab_name = ab_prefix + i;
+ MailServices.ab.newAddressBook(
+ ab_name,
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ dump("created: " + ab_name + "\n");
+
+ for (var j = 1; j < 2; j++) {
+ for (let elem of MailServices.ab.directories) {
+ let uri = elem.URI;
+ let dir = MailServices.ab.getDirectory(uri);
+
+ dump("considering: j: " + j + " " + elem.dirName + "\n");
+
+ if (j == 1 && elem.dirName.startsWith(ab_prefix)) {
+ for (i = 1; i <= 1000; i++) {
+ let abCard = Cc["@mozilla.org/addressbook/cardproperty;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsIAbCard);
+
+ for (key in card_properties) {
+ abCard.setProperty(key, card_properties[key]);
+ }
+
+ abCard = dir.addCard(abCard);
+ }
+ dump("populated: " + elem.dirName + "\n");
+ }
+ }
+ }
+ }
+}
+
+function bug_537815_test() {
+ for (let elem of MailServices.ab.directories) {
+ let uri = elem.URI;
+ let dir = MailServices.ab.getDirectory(uri);
+ if (elem.dirName.startsWith(ab_prefix)) {
+ for (let abCard of dir.childCards) {
+ for (let key in card_properties) {
+ abCard.getProperty(key, null);
+ }
+ }
+ dump("visited all cards in: " + elem.dirName + "\n");
+ }
+ }
+}
+
+function test_bug_537815() {
+ bug_537815_fixture_setup();
+ bug_537815_test();
+ bug_537815_fixture_tear_down();
+}
+
+function bug_537815_fixture_tear_down() {
+ let a_uri = {};
+ for (let elem of MailServices.ab.directories) {
+ if (elem.dirName.startsWith(ab_prefix)) {
+ a_uri[elem.URI] = true;
+ dump("to be deleted: " + elem.dirName + "\n");
+ }
+ }
+
+ for (let uri in a_uri) {
+ MailServices.ab.deleteAddressBook(uri);
+ }
+}
+
+function run_test() {
+ test_bug_537815();
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_delete_book.js b/comm/mailnews/addrbook/test/unit/test_delete_book.js
new file mode 100644
index 0000000000..8c63bb43b0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_delete_book.js
@@ -0,0 +1,82 @@
+/* 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 { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function getExistingDirectories() {
+ return MailServices.ab.directories.map(d => d.dirPrefId);
+}
+
+add_task(async function clearPref() {
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "delete me",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.deleteme",
+ "ldap_2.servers.history",
+ ]);
+ Services.prefs.setStringPref("mail.collect_addressbook", book.URI);
+
+ await promiseDirectoryRemoved(book.URI);
+
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+});
+
+add_task(async function protectBuiltIns() {
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("this is completely wrong");
+ }, /NS_ERROR_MALFORMED_URI/);
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("jsaddrbook://bad.sqlite");
+ }, /NS_ERROR_UNEXPECTED/);
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("jsaddrbook://history.sqlite");
+ }, /NS_ERROR_FAILURE/);
+ Assert.throws(() => {
+ MailServices.ab.deleteAddressBook("jsaddrbook://abook.sqlite");
+ }, /NS_ERROR_FAILURE/);
+
+ Assert.deepEqual(getExistingDirectories(), [
+ "ldap_2.servers.pab",
+ "ldap_2.servers.history",
+ ]);
+ equal(
+ Services.prefs.getStringPref("mail.collect_addressbook"),
+ "jsaddrbook://history.sqlite"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_export.js b/comm/mailnews/addrbook/test/unit/test_export.js
new file mode 100644
index 0000000000..34874e2f69
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_export.js
@@ -0,0 +1,156 @@
+/* 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 { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+var { AddrBookUtils } = ChromeUtils.import(
+ "resource:///modules/AddrBookUtils.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+async function subtest(cardConstructor) {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ let contact1 = cardConstructor();
+ contact1.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ contact1.displayName = "contact number one";
+ contact1.firstName = "contact";
+ contact1.lastName = "one";
+ contact1.primaryEmail = "contact1@invalid";
+ contact1 = book.addCard(contact1);
+
+ let contact2 = cardConstructor();
+ contact2.UID = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+ contact2.displayName = "contact number two";
+ contact2.firstName = "contact";
+ contact2.lastName = "two";
+ contact2.primaryEmail = "contact2@invalid";
+ if (contact2.supportsVCard) {
+ contact2.vCardProperties.addValue("title", `"worker"`);
+ contact2.vCardProperties.addValue("note", "here's some unicode text…");
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom1", {}, "text", "custom, 1")
+ );
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom2", {}, "text", "custom\t2")
+ );
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom3", {}, "text", "custom\r3")
+ );
+ contact2.vCardProperties.addEntry(
+ new VCardPropertyEntry("x-custom4", {}, "text", "custom\n4")
+ );
+ } else {
+ contact2.setProperty("JobTitle", `"worker"`);
+ contact2.setProperty("Notes", "here's some unicode text…");
+ contact2.setProperty("Custom1", "custom, 1");
+ contact2.setProperty("Custom2", "custom\t2");
+ contact2.setProperty("Custom3", "custom\r3");
+ contact2.setProperty("Custom4", "custom\n4");
+ }
+ contact2 = book.addCard(contact2);
+
+ let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "new list";
+ list = book.addMailList(list);
+ list.addCard(contact1);
+
+ await compareAgainstFile(
+ "export.csv",
+ AddrBookUtils.exportDirectoryToDelimitedText(book, ",")
+ );
+ await compareAgainstFile(
+ "export.txt",
+ AddrBookUtils.exportDirectoryToDelimitedText(book, "\t")
+ );
+ await compareAgainstFile(
+ "export.vcf",
+ AddrBookUtils.exportDirectoryToVCard(book)
+ );
+ // modifytimestamp is always changing, replace it with a fixed value.
+ await compareAgainstFile(
+ "export.ldif",
+ AddrBookUtils.exportDirectoryToLDIF(book).replace(
+ /modifytimestamp: \d+/g,
+ "modifytimestamp: 12345"
+ )
+ );
+}
+
+async function compareAgainstFile(fileName, actual) {
+ info(`checking against ${fileName}`);
+
+ // The test files are UTF-8 encoded and have Windows line endings. The
+ // exportDirectoryTo* functions are platform-dependent, except for VCard
+ // which always uses Windows line endings.
+
+ let file = do_get_file(`data/${fileName}`);
+ let expected = await IOUtils.readUTF8(file.path);
+
+ if (AppConstants.platform != "win" && fileName != "export.vcf") {
+ expected = expected.replace(/\r\n/g, "\n");
+ }
+
+ // From here on, \r is just another character. It will be the last character
+ // on lines where Windows line endings exist.
+ let expectedLines = expected.split("\n");
+ let actualLines = actual.split("\n");
+ info(actual);
+ Assert.deepEqual(actualLines.sort(), expectedLines.sort());
+ // equal(actualLines.length, expectedLines.length, "correct number of lines");
+
+ // for (let l = 0; l < expectedLines.length; l++) {
+ // let expectedLine = expectedLines[l];
+ // let actualLine = actualLines[l];
+ // if (actualLine == expectedLine) {
+ // ok(true, `line ${l + 1} matches`);
+ // } else {
+ // for (let c = 0; c < expectedLine.length && c < actualLine.length; c++) {
+ // if (actualLine[c] != expectedLine[c]) {
+ // // This call to equal automatically prints some extra characters of
+ // // context. Hopefully that helps with debugging.
+ // equal(
+ // actualLine.substring(c - 10, c + 10),
+ // expectedLine.substring(c - 10, c + 10),
+ // `line ${l + 1} does not match at character ${c + 1}`
+ // );
+ // }
+ // }
+ // equal(
+ // expectedLine.length,
+ // actualLine.length,
+ // `line ${l + 1} lengths differ`
+ // );
+ // }
+ // }
+}
+
+add_task(async function addrBookCard() {
+ return subtest(() => new AddrBookCard());
+});
+
+add_task(async function cardProperty() {
+ return subtest(() =>
+ Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard)
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js b/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js
new file mode 100644
index 0000000000..957285bbba
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js
@@ -0,0 +1,420 @@
+/* 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 FILE_NAME = "abook-1.sqlite";
+var SCHEME = "jsaddrbook";
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var book, contact, list, listCard;
+var observer = {
+ topics: [
+ "addrbook-directory-created",
+ "addrbook-directory-updated",
+ "addrbook-directory-deleted",
+ "addrbook-contact-created",
+ "addrbook-contact-updated",
+ "addrbook-contact-properties-updated",
+ "addrbook-contact-deleted",
+ "addrbook-list-created",
+ "addrbook-list-updated",
+ "addrbook-list-deleted",
+ "addrbook-list-member-added",
+ "addrbook-list-member-removed",
+ ],
+ setUp() {
+ for (let topic of this.topics) {
+ Services.obs.addObserver(observer, topic);
+ }
+ },
+ cleanUp() {
+ for (let topic of this.topics) {
+ Services.obs.removeObserver(observer, topic);
+ }
+ },
+
+ events: [],
+ observe(subject, topic, data) {
+ this.events.push([topic, subject, data]);
+ },
+ checkEvents(...events) {
+ info(
+ "Actual events: " +
+ JSON.stringify(
+ observer.events.map(e =>
+ e.map(a => {
+ if (a instanceof Ci.nsIAbDirectory) {
+ return `[nsIAbDirectory]`;
+ }
+ if (a instanceof Ci.nsIAbCard) {
+ return `[nsIAbCard]`;
+ }
+ return a;
+ })
+ )
+ )
+ );
+ equal(observer.events.length, events.length);
+
+ let actualEvents = observer.events.slice();
+ observer.events.length = 0;
+
+ for (let j = 0; j < events.length; j++) {
+ let expectedEvent = events[j];
+ let actualEvent = actualEvents[j];
+
+ for (let i = 0; i < expectedEvent.length; i++) {
+ try {
+ expectedEvent[i].QueryInterface(Ci.nsIAbCard);
+ ok(actualEvent[i].equals(expectedEvent[i]));
+ } catch (ex) {
+ if (expectedEvent[i] instanceof Ci.nsIAbDirectory) {
+ equal(actualEvent[i].UID, expectedEvent[i].UID);
+ } else if (expectedEvent[i] === null) {
+ ok(!actualEvent[i]);
+ } else if (expectedEvent[i] !== undefined) {
+ equal(actualEvent[i], expectedEvent[i]);
+ }
+ }
+ }
+ }
+
+ return actualEvents;
+ },
+};
+
+var baseAddressBookCount;
+
+add_setup(function () {
+ let profileDir = do_get_profile();
+ observer.setUp();
+
+ let dirs = MailServices.ab.directories;
+ // On Mac we might be loading the OS X Address Book. If we are, then we
+ // need to take acccount of that here, so that the test still pass on
+ // development machines.
+ if (
+ AppConstants.platform == "macosx" &&
+ dirs[0].URI == "moz-abosxdirectory:///"
+ ) {
+ equal(dirs.length, 3);
+ equal(dirs[1].fileName, kPABData.fileName);
+ equal(dirs[2].fileName, kCABData.fileName);
+ } else {
+ equal(dirs.length, 2);
+ equal(dirs[0].fileName, kPABData.fileName);
+ equal(dirs[1].fileName, kCABData.fileName);
+ }
+ // Also record the address book counts so that we get the expected counts
+ // correct further down in the test.
+ baseAddressBookCount = dirs.length;
+
+ // Check the PAB file was created.
+ let pabFile = profileDir.clone();
+ pabFile.append(kPABData.fileName);
+ ok(pabFile.exists());
+
+ // Check the CAB file was created.
+ let cabFile = profileDir.clone();
+ cabFile.append(kCABData.fileName);
+ ok(cabFile.exists());
+});
+
+add_task(async function createAddressBook() {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ observer.checkEvents(["addrbook-directory-created", book]);
+
+ // Check nsIAbDirectory properties.
+ ok(!book.readOnly);
+ ok(!book.isRemote);
+ ok(!book.isSecure);
+ equal(book.dirName, "new book");
+ equal(book.dirType, Ci.nsIAbManager.JS_DIRECTORY_TYPE);
+ equal(book.fileName, FILE_NAME);
+ equal(book.UID.length, 36);
+ equal(book.URI, `${SCHEME}://${FILE_NAME}`);
+ equal(book.isMailList, false);
+ equal(book.supportsMailingLists, true);
+ equal(book.dirPrefId, "ldap_2.servers.newbook");
+
+ // Check enumerations.
+ equal(Array.from(book.childNodes).length, 0);
+ equal(Array.from(book.childCards).length, 0);
+
+ // Check prefs.
+ equal(
+ Services.prefs.getStringPref("ldap_2.servers.newbook.description"),
+ "new book"
+ );
+ equal(
+ Services.prefs.getIntPref("ldap_2.servers.newbook.dirType"),
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ equal(
+ Services.prefs.getStringPref("ldap_2.servers.newbook.filename"),
+ FILE_NAME
+ );
+ equal(Services.prefs.getStringPref("ldap_2.servers.newbook.uid"), book.UID);
+ equal(MailServices.ab.directories.length, baseAddressBookCount + 1);
+
+ // Check the file was created.
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append(FILE_NAME);
+ ok(dbFile.exists());
+});
+
+add_task(async function editAddressBook() {
+ book.dirName = "updated book";
+ observer.checkEvents(["addrbook-directory-updated", book, "DirName"]);
+ equal(book.dirName, "updated book");
+ equal(
+ Services.prefs.getStringPref("ldap_2.servers.newbook.description"),
+ "updated book"
+ );
+});
+
+add_task(async function createContact() {
+ contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ contact.displayName = "a new contact";
+ contact.firstName = "new";
+ contact.lastName = "contact";
+ contact.primaryEmail = "test@invalid";
+ contact.setProperty("Foo", "This will be deleted later.");
+ contact = book.addCard(contact);
+ observer.checkEvents(["addrbook-contact-created", contact, book.UID]);
+
+ let cards = book.childCards;
+ equal(cards.length, 1);
+ ok(cards[0].equals(contact));
+
+ // Check nsIAbCard properties.
+ equal(contact.directoryUID, book.UID);
+ equal(contact.UID.length, 36);
+ equal(contact.firstName, "new");
+ equal(contact.lastName, "contact");
+ equal(contact.displayName, "a new contact");
+ equal(contact.primaryEmail, "test@invalid");
+ equal(contact.getProperty("Foo", ""), "This will be deleted later.");
+ equal(contact.isMailList, false);
+ let modifiedDate = parseInt(contact.getProperty("LastModifiedDate", ""), 10);
+ Assert.lessOrEqual(modifiedDate, Date.now() / 1000);
+ Assert.greater(modifiedDate, Date.now() / 1000 - 10);
+
+ // Check nsIAbCard methods.
+ equal(
+ contact.generateName(Ci.nsIAbCard.GENERATE_DISPLAY_NAME),
+ "a new contact"
+ );
+ equal(
+ contact.generateName(Ci.nsIAbCard.GENERATE_LAST_FIRST_ORDER),
+ "contact, new"
+ );
+ equal(
+ contact.generateName(Ci.nsIAbCard.GENERATE_FIRST_LAST_ORDER),
+ "new contact"
+ );
+});
+
+add_task(async function editContact() {
+ contact.firstName = "updated";
+ contact.lastName = "contact";
+ contact.displayName = "updated contact";
+ contact.setProperty("Foo", null);
+ contact.setProperty("Bar1", "a new property");
+ contact.setProperty("Bar2", "");
+ contact.setProperty("LastModifiedDate", 0);
+ book.modifyCard(contact);
+ let [, propertyEvent] = observer.checkEvents(
+ ["addrbook-contact-updated", contact, book.UID],
+ ["addrbook-contact-properties-updated", contact]
+ );
+ Assert.deepEqual(JSON.parse(propertyEvent[2]), {
+ DisplayName: {
+ oldValue: "a new contact",
+ newValue: "updated contact",
+ },
+ Foo: {
+ oldValue: "This will be deleted later.",
+ newValue: null,
+ },
+ Bar1: {
+ oldValue: null,
+ newValue: "a new property",
+ },
+ FirstName: {
+ oldValue: "new",
+ newValue: "updated",
+ },
+ _vCard: {
+ oldValue: formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ FN:a new contact
+ EMAIL;PREF=1:test@invalid
+ N:contact;new;;;
+ UID:${contact.UID}
+ END:VCARD`,
+ newValue: formatVCard`
+ BEGIN:VCARD
+ VERSION:4.0
+ FN:updated contact
+ EMAIL;PREF=1:test@invalid
+ N:contact;updated;;;
+ UID:${contact.UID}
+ END:VCARD`,
+ },
+ });
+ contact = book.childCards[0];
+ equal(contact.firstName, "updated");
+ equal(contact.lastName, "contact");
+ equal(contact.displayName, "updated contact");
+ equal(contact.getProperty("Foo", "empty"), "empty");
+ equal(contact.getProperty("Bar1", ""), "a new property");
+ equal(contact.getProperty("Bar2", "no value"), "no value");
+ let modifiedDate = parseInt(contact.getProperty("LastModifiedDate", ""), 10);
+ Assert.lessOrEqual(modifiedDate, Date.now() / 1000);
+ Assert.greater(modifiedDate, Date.now() / 1000 - 10);
+});
+
+add_task(async function createMailingList() {
+ list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(
+ Ci.nsIAbDirectory
+ );
+ list.isMailList = true;
+ list.dirName = "new list";
+ list = book.addMailList(list);
+ // Skip checking events temporarily, until listCard is defined.
+
+ // Check enumerations.
+ let childNodes = book.childNodes;
+ equal(childNodes.length, 1);
+ equal(childNodes[0].UID, list.UID); // TODO Object equality doesn't work because of XPCOM.
+ let childCards = book.childCards;
+ equal(childCards.length, 2);
+ if (childCards[0].isMailList) {
+ listCard = childCards[0];
+ ok(childCards[1].equals(contact));
+ } else {
+ ok(childCards[0].equals(contact));
+ listCard = childCards[1];
+ }
+ equal(listCard.UID, list.UID);
+
+ observer.checkEvents(["addrbook-list-created", list, book.UID]);
+
+ // Check nsIAbDirectory properties.
+ equal(list.dirName, "new list");
+ equal(list.UID.length, 36);
+ equal(list.URI, `${SCHEME}://${FILE_NAME}/${list.UID}`);
+ equal(list.isMailList, true);
+ equal(list.supportsMailingLists, false);
+
+ // Check list enumerations.
+ equal(Array.from(list.childNodes).length, 0);
+ equal(Array.from(list.childCards).length, 0);
+
+ // Check nsIAbCard properties.
+ equal(listCard.firstName, "");
+ equal(listCard.lastName, "new list");
+ equal(listCard.primaryEmail, "");
+ equal(listCard.displayName, "new list");
+});
+
+add_task(async function editMailingList() {
+ list.dirName = "updated list";
+ list.editMailListToDatabase(null);
+ observer.checkEvents(["addrbook-list-updated", list, book.UID]);
+ equal("updated list", list.dirName);
+});
+
+add_task(async function addMailingListMember() {
+ list.addCard(contact);
+ observer.checkEvents(["addrbook-list-member-added", contact, list.UID]);
+
+ // Check list enumerations.
+ equal(Array.from(list.childNodes).length, 0);
+ let childCards = list.childCards;
+ equal(childCards.length, 1);
+ ok(childCards[0].equals(contact));
+});
+
+add_task(async function removeMailingListMember() {
+ list.deleteCards([contact]);
+ observer.checkEvents(["addrbook-list-member-removed", contact, list.UID]);
+
+ // Check list enumerations.
+ equal(Array.from(list.childNodes).length, 0);
+ equal(Array.from(list.childCards).length, 0);
+});
+
+add_task(async function deleteMailingList() {
+ book.deleteDirectory(list);
+ observer.checkEvents(["addrbook-list-deleted", list, book.UID]);
+});
+
+add_task(async function deleteContact() {
+ book.deleteCards([contact]);
+ observer.checkEvents(["addrbook-contact-deleted", contact, book.UID]);
+
+ // Check enumerations.
+ equal(Array.from(book.childNodes).length, 0);
+ equal(Array.from(book.childCards).length, 0);
+});
+
+// Tests that the UID on a new contact can be set.
+add_task(async function createContactWithUID() {
+ let contactWithUID = Cc[
+ "@mozilla.org/addressbook/cardproperty;1"
+ ].createInstance(Ci.nsIAbCard);
+ contactWithUID.UID = "I'm a UID!";
+ contactWithUID = book.addCard(contactWithUID);
+ equal("I'm a UID!", contactWithUID.UID, "New contact has the UID we set");
+
+ Assert.throws(() => {
+ // Set the UID after it already exists.
+ contactWithUID.UID = "This should not be possible";
+ }, /NS_ERROR_UNEXPECTED/);
+
+ // Setting the UID to it's existing value should not fail.
+ contactWithUID.UID = contactWithUID.UID; // eslint-disable-line no-self-assign
+
+ book.deleteCards([contactWithUID]);
+ observer.events.length = 0;
+});
+
+add_task(async function deleteAddressBook() {
+ await promiseDirectoryRemoved(book.URI);
+
+ observer.checkEvents(["addrbook-directory-deleted", book, null]);
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.dirType"));
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.description"));
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.filename"));
+ ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.uid"));
+ let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ dbFile.append(FILE_NAME);
+ ok(!dbFile.exists());
+ equal(MailServices.ab.directories.length, baseAddressBookCount);
+ Assert.throws(() => {
+ MailServices.ab.getDirectory(`${SCHEME}://${FILE_NAME}`);
+ }, /NS_ERROR_FAILURE/);
+});
+
+add_task(async function cleanUp() {
+ observer.checkEvents();
+ observer.cleanUp();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_ldap1.js b/comm/mailnews/addrbook/test/unit/test_ldap1.js
new file mode 100644
index 0000000000..e323d71386
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldap1.js
@@ -0,0 +1,205 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for basic LDAP address book functions
+ */
+
+var kLDAPUriPrefix = "moz-abldapdirectory://";
+var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)";
+
+function run_test() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ let abCountBeforeStart = MailServices.ab.directories.length;
+
+ // Test - Create an LDAP directory
+ let abUri = MailServices.ab.newAddressBook(
+ "test",
+ kLDAPTestSpec,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ let abCountAfterCreate = MailServices.ab.directories.length;
+ Assert.equal(abCountAfterCreate, abCountBeforeStart + 1);
+
+ // Test - Check we have the directory.
+ let abDir = MailServices.ab
+ .getDirectory(kLDAPUriPrefix + abUri)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ // Test - Check various fields
+ Assert.equal(abDir.dirName, "test");
+ Assert.equal(abDir.lDAPURL.spec, kLDAPTestSpec);
+ Assert.ok(abDir.readOnly);
+
+ // Test - Write a UTF-8 Auth DN and check it
+ abDir.authDn = "test\u00D0";
+
+ Assert.equal(abDir.authDn, "test\u00D0");
+
+ // Test - searchDuringLocalAutocomplete
+
+ // Set up an account and identity in the account manager
+ let identity = MailServices.accounts.createIdentity();
+
+ const localAcTests = [
+ // Online checks
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: false,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: false,
+ result: false,
+ },
+ // Offline checks with and without global prefs set, no identity key
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: false,
+ idSer: "",
+ idKey: "",
+ offline: true,
+ result: true,
+ },
+ // Offline checks with and without global prefs set, with identity key
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: "",
+ idOver: false,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: false,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: true,
+ },
+ // Offline checks, no global prefs, identity ones only
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: true,
+ idSer: "",
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: true,
+ idSer: kPABData.dirPrefID,
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: true,
+ idSer: abDir.dirPrefId,
+ idKey: identity.key,
+ offline: true,
+ result: true,
+ },
+ {
+ useDir: false,
+ dirSer: "",
+ idOver: false,
+ idSer: abDir.dirPrefId,
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ // Offline checks, global prefs and identity ones
+ {
+ useDir: true,
+ dirSer: kPABData.dirPrefID,
+ idOver: true,
+ idSer: abDir.dirPrefId,
+ idKey: identity.key,
+ offline: true,
+ result: true,
+ },
+ {
+ useDir: true,
+ dirSer: abDir.dirPrefId,
+ idOver: true,
+ idSer: kPABData.dirPrefID,
+ idKey: identity.key,
+ offline: true,
+ result: false,
+ },
+ ];
+
+ function checkAc(element, index, array) {
+ dump("Testing index " + index + "\n");
+ Services.prefs.setBoolPref(
+ "ldap_2.autoComplete.useDirectory",
+ element.useDir
+ );
+ Services.prefs.setCharPref(
+ "ldap_2.autoComplete.directoryServer",
+ element.dirSer
+ );
+ identity.overrideGlobalPref = element.idOver;
+ identity.directoryServer = element.idSer;
+ Services.io.offline = element.offline;
+
+ Assert.equal(abDir.useForAutocomplete(element.idKey), element.result);
+ }
+
+ localAcTests.forEach(checkAc);
+
+ MailServices.ab.deleteAddressBook(abDir.URI);
+
+ let abCountAfterDelete = MailServices.ab.directories.length;
+ Assert.equal(abCountAfterDelete, abCountBeforeStart);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_ldap2.js b/comm/mailnews/addrbook/test/unit/test_ldap2.js
new file mode 100644
index 0000000000..2dc39c4a86
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldap2.js
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for bug 532170. LDAP address book named with cyrillic/chinese
+ * letters doesn't work.
+ */
+
+var kLDAPUriPrefix = "moz-abldapdirectory://";
+var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)";
+
+function run_test() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ // Test - Create an LDAP directory
+
+ // Use a UTF-8 based directory name
+ var abUri = MailServices.ab.newAddressBook(
+ "\u041C\u0435\u043B\u0435\u043D\u043A\u0438",
+ kLDAPTestSpec,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ // Test - Check we have the directory.
+ let abDir = MailServices.ab
+ .getDirectory(kLDAPUriPrefix + abUri)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ // Test - Check various fields
+ Assert.equal(abDir.dirName, "\u041C\u0435\u043B\u0435\u043D\u043A\u0438");
+ Assert.equal(abDir.lDAPURL.spec, kLDAPTestSpec);
+ Assert.ok(abDir.readOnly);
+
+ // XXX I'd really like a better check than this, to check that searching
+ // works correctly. However we haven't got the support for that at the moment
+ // and this at least ensures that we get a consistent ascii based preference
+ // for the directory.
+ Assert.equal(abDir.dirPrefId, "ldap_2.servers._nonascii");
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_ldapOffline.js b/comm/mailnews/addrbook/test/unit/test_ldapOffline.js
new file mode 100644
index 0000000000..ed81344d03
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldapOffline.js
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite to check that we correctly get child cards for LDAP directories
+ * when offline and that we don't crash.
+ */
+
+var kLDAPUriPrefix = "moz-abldapdirectory://";
+var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)";
+
+// Main function for the this test so we can check both personal and
+// collected books work correctly in an easy manner.
+function run_test() {
+ // If nsIAbLDAPDirectory doesn't exist in our build options, someone has
+ // specified --disable-ldap
+ if (!("nsIAbLDAPDirectory" in Ci)) {
+ return;
+ }
+
+ // Test set-up
+ let abUri = MailServices.ab.newAddressBook(
+ "test",
+ kLDAPTestSpec,
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE
+ );
+
+ let abDir = MailServices.ab
+ .getDirectory(kLDAPUriPrefix + abUri)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ const kLDAPFileName = "ldap-1.sqlite";
+
+ // Test setup - copy the data file into place
+ loadABFile("data/cardForEmail", kLDAPFileName);
+
+ // And tell the ldap directory we want this file.
+ abDir.replicationFileName = kLDAPFileName;
+
+ // Now go offline
+ Services.io.offline = true;
+
+ // Make sure we clear any memory that is now loose, so that the crash would
+ // be triggered.
+ gc();
+
+ // Now try and get the card that has been replicated for offline use.
+ Assert.equal(abDir.childCards.length, 5);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_ldapReplication.js b/comm/mailnews/addrbook/test/unit/test_ldapReplication.js
new file mode 100644
index 0000000000..220417a095
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldapReplication.js
@@ -0,0 +1,159 @@
+/* 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/. */
+
+const { LDAPServer } = ChromeUtils.import(
+ "resource://testing-common/LDAPServer.jsm"
+);
+
+const autocompleteService = Cc[
+ "@mozilla.org/autocomplete/search;1?name=addrbook"
+].getService(Ci.nsIAutoCompleteSearch);
+const jsonFile = do_get_file("data/ldap_contacts.json");
+const replicationService = Cc[
+ "@mozilla.org/addressbook/ldap-replication-service;1"
+].getService(Ci.nsIAbLDAPReplicationService);
+
+add_task(async () => {
+ LDAPServer.open();
+ let ldapContacts = await IOUtils.readJSON(jsonFile.path);
+
+ let bookPref = MailServices.ab.newAddressBook(
+ "XPCShell",
+ `ldap://localhost:${LDAPServer.port}/people??sub?(objectclass=*)`,
+ 0
+ );
+ let book = MailServices.ab.getDirectoryFromId(bookPref);
+ book.QueryInterface(Ci.nsIAbLDAPDirectory);
+ equal(book.replicationFileName, "ldap.sqlite");
+
+ Services.prefs.setCharPref("ldap_2.autoComplete.directoryServer", bookPref);
+ Services.prefs.setBoolPref("ldap_2.autoComplete.useDirectory", true);
+
+ registerCleanupFunction(async () => {
+ LDAPServer.close();
+ });
+
+ let progressResolve;
+ let progressPromise = new Promise(resolve => (progressResolve = resolve));
+ let progressListener = {
+ onStateChange(webProgress, request, stateFlags, status) {
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
+ info("replication started");
+ }
+ if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
+ info("replication ended");
+ progressResolve();
+ }
+ },
+ onProgressChange(
+ webProgress,
+ request,
+ currentSelfProgress,
+ maxSelfProgress,
+ currentTotalProgress,
+ maxTotalProgress
+ ) {},
+ onLocationChange(webProgress, request, location, flags) {},
+ onStatusChange(webProgress, request, status, message) {},
+ onSecurityChange(webProgress, request, state) {},
+ onContentBlockingEvent(webProgress, request, event) {},
+ };
+
+ replicationService.startReplication(book, progressListener);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ for (let contact of Object.values(ldapContacts)) {
+ LDAPServer.writeSearchResultEntry(contact);
+ }
+ LDAPServer.writeSearchResultDone();
+
+ await progressPromise;
+ equal(book.replicationFileName, "ldap.sqlite");
+
+ Services.io.offline = true;
+
+ let cards = book.childCards;
+ deepEqual(cards.map(c => c.displayName).sort(), [
+ "Eurus Holmes",
+ "Greg Lestrade",
+ "Irene Adler",
+ "Jim Moriarty",
+ "John Watson",
+ "Mary Watson",
+ "Molly Hooper",
+ "Mrs Hudson",
+ "Mycroft Holmes",
+ "Sherlock Holmes",
+ ]);
+
+ await new Promise(resolve => {
+ autocompleteService.startSearch("molly", '{"type":"addr_to"}', null, {
+ onSearchResult(search, result) {
+ equal(result.matchCount, 1);
+ equal(result.getValueAt(0), "Molly Hooper <molly@bakerstreet.invalid>");
+ resolve();
+ },
+ });
+ });
+ await new Promise(resolve => {
+ autocompleteService.startSearch("watson", '{"type":"addr_to"}', null, {
+ onSearchResult(search, result) {
+ equal(result.matchCount, 2);
+ equal(result.getValueAt(0), "John Watson <john@bakerstreet.invalid>");
+ equal(result.getValueAt(1), "Mary Watson <mary@bakerstreet.invalid>");
+ resolve();
+ },
+ });
+ });
+
+ // Do it again with different information from the server. Ensure we have the new information.
+
+ progressPromise = new Promise(resolve => (progressResolve = resolve));
+ replicationService.startReplication(book, progressListener);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.eurus);
+ LDAPServer.writeSearchResultEntry(ldapContacts.mary);
+ LDAPServer.writeSearchResultEntry(ldapContacts.molly);
+ LDAPServer.writeSearchResultDone();
+
+ await progressPromise;
+ equal(book.replicationFileName, "ldap.sqlite");
+
+ cards = book.childCards;
+ deepEqual(cards.map(c => c.displayName).sort(), [
+ "Eurus Holmes",
+ "Mary Watson",
+ "Molly Hooper",
+ ]);
+
+ // Do it again but cancel. Ensure we still have the old information.
+
+ progressPromise = new Promise(resolve => (progressResolve = resolve));
+ replicationService.startReplication(book, progressListener);
+
+ await LDAPServer.read(LDAPServer.BindRequest);
+ LDAPServer.writeBindResponse();
+
+ await LDAPServer.read(LDAPServer.SearchRequest);
+ LDAPServer.writeSearchResultEntry(ldapContacts.john);
+ LDAPServer.writeSearchResultEntry(ldapContacts.sherlock);
+ LDAPServer.writeSearchResultEntry(ldapContacts.mrs_hudson);
+ replicationService.cancelReplication(book);
+
+ await progressPromise;
+
+ cards = book.childCards;
+ deepEqual(cards.map(c => c.displayName).sort(), [
+ "Eurus Holmes",
+ "Mary Watson",
+ "Molly Hooper",
+ ]);
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_ldapquery.js b/comm/mailnews/addrbook/test/unit/test_ldapquery.js
new file mode 100644
index 0000000000..90b1f1673d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_ldapquery.js
@@ -0,0 +1,181 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+/**
+ * Test basic LDAP querying.
+ */
+
+const { LDAPDaemon, LDAPHandlerFn } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Ldapd.jsm"
+);
+const { BinaryServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Binaryd.jsm"
+);
+
+/**
+ * Adaptor class to implement nsILDAPMessageListener with a promise.
+ * It should be passed into LDAP functions as a normal listener. The
+ * caller can then await the promise attribute.
+ * Based on the pattern used in PromiseTestUtils.jsm.
+ *
+ * This base class just rejects all callbacks. Derived classes should
+ * implement the callbacks they need to handle.
+ *
+ * @implements {nsILDAPMessageListener}
+ */
+class PromiseListener {
+ constructor() {
+ this.QueryInterface = ChromeUtils.generateQI(["nsILDAPMessageListener"]);
+ this.promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ }
+ onLDAPMessage(message) {
+ this._reject(new Error("Unexpected onLDAPMessage"));
+ }
+ onLDAPInit() {
+ this._reject(new Error("Unexpected onLDAPInit"));
+ }
+ onLDAPError(status, secInfo, location) {
+ this._reject(new Error(`Unexpected onLDAPError (0x${status.toString(16)}`));
+ }
+}
+
+/**
+ * PromiseInitListener resolves the promise when onLDAPInit is called.
+ *
+ * @augments {PromiseListener}
+ */
+class PromiseInitListener extends PromiseListener {
+ onLDAPInit() {
+ this._resolve();
+ }
+}
+
+/**
+ * PromiseBindListener resolves when a bind operation completes.
+ *
+ * @augments {PromiseListener}
+ */
+class PromiseBindListener extends PromiseListener {
+ onLDAPMessage(message) {
+ if (Ci.nsILDAPErrors.SUCCESS != message.errorCode) {
+ this._reject(
+ new Error(`Operation failed (LDAP code ${message.errorCode})`)
+ );
+ }
+ if (Ci.nsILDAPMessage.RES_BIND == message.type) {
+ this._resolve(); // All done.
+ }
+ }
+}
+
+/**
+ * PromiseSearchListener collects search results, returning them via promise
+ * when the search is complete.
+ *
+ * @augments {PromiseListener}
+ */
+class PromiseSearchListener extends PromiseListener {
+ constructor() {
+ super();
+ this._results = [];
+ }
+ onLDAPMessage(message) {
+ if (Ci.nsILDAPMessage.RES_SEARCH_RESULT == message.type) {
+ this._resolve(this._results); // All done.
+ }
+ if (Ci.nsILDAPMessage.RES_SEARCH_ENTRY == message.type) {
+ this._results.push(message);
+ }
+ }
+}
+
+add_task(async function test_basic_query() {
+ // Load in some test contact data (characters from Sherlock Holmes).
+ let raw = await IOUtils.readUTF8(
+ do_get_file(
+ "../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json"
+ ).path
+ );
+ let testContacts = JSON.parse(raw);
+
+ // Set up fake LDAP server, loaded with the test contacts.
+ let daemon = new LDAPDaemon();
+ daemon.add(...Object.values(testContacts));
+ // daemon.setDebug(true);
+ let server = new BinaryServer(LDAPHandlerFn, daemon);
+ server.start();
+
+ // Connect to the fake server.
+ let url = `ldap://localhost:${server.port}`;
+ let ldapURL = Services.io.newURI(url).QueryInterface(Ci.nsILDAPURL);
+ let conn = Cc["@mozilla.org/network/ldap-connection;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsILDAPConnection);
+
+ // Initialisation is async.
+ let initListener = new PromiseInitListener();
+ conn.init(ldapURL, null, initListener, null, Ci.nsILDAPConnection.VERSION3);
+ await initListener.promise;
+
+ // Perform bind.
+ let bindListener = new PromiseBindListener();
+ let bindOp = Cc["@mozilla.org/network/ldap-operation;1"].createInstance(
+ Ci.nsILDAPOperation
+ );
+ bindOp.init(conn, bindListener, null);
+ bindOp.simpleBind(""); // no password
+ await bindListener.promise;
+
+ // Run a search.
+ let searchListener = new PromiseSearchListener();
+ let searchOp = Cc["@mozilla.org/network/ldap-operation;1"].createInstance(
+ Ci.nsILDAPOperation
+ );
+ searchOp.init(conn, searchListener, null);
+ searchOp.searchExt(
+ "", // dn
+ Ci.nsILDAPURL.SCOPE_SUBTREE,
+ "(sn=Holmes)", // filter: Find the Holmes family members.
+ "", // wanted_attributes
+ 0, // timeOut
+ 100 // maxEntriesWanted
+ );
+ let matches = await searchListener.promise;
+
+ // Make sure we got the contacts we expected (just use cn for comparing):
+ const holmesCNs = ["Eurus Holmes", "Mycroft Holmes", "Sherlock Holmes"];
+ const holmesGivenNames = ["Eurus", "Mycroft", "Sherlock"];
+ const nonHolmesCNs = [
+ "Greg Lestrade",
+ "Irene Adler",
+ "Jim Moriarty",
+ "John Watson",
+ "Mary Watson",
+ "Molly Hooper",
+ "Mrs Hudson",
+ ];
+ let cns = matches.map(ent => ent.getValues("cn")[0]);
+ cns.sort();
+ Assert.deepEqual(cns, holmesCNs);
+
+ // Test getValues is case insensitive about the attribute name.
+ let givenNames = matches.map(ent => ent.getValues("givenname")[0]);
+ givenNames.sort();
+ Assert.deepEqual(givenNames, holmesGivenNames);
+ givenNames = matches.map(ent => ent.getValues("givenName")[0]);
+ givenNames.sort();
+ Assert.deepEqual(givenNames, holmesGivenNames);
+ givenNames = matches.map(ent => ent.getValues("GIVENNAME")[0]);
+ givenNames.sort();
+ Assert.deepEqual(givenNames, holmesGivenNames);
+
+ // Sanity check: make sure the non-Holmes contacts were excluded.
+ nonHolmesCNs.forEach(cn => Assert.ok(!cns.includes(cn)));
+
+ server.stop();
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_mailList1.js b/comm/mailnews/addrbook/test/unit/test_mailList1.js
new file mode 100644
index 0000000000..0889257da6
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_mailList1.js
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for mailing list functions.
+ *
+ * This suite relies on abLists1.mab. checkLists requires that the mailing list
+ * name be "TestList<n>" where <n> is the number of the list that also matches
+ * the <n> in the uri: moz-ab???directory://path/MailList<n>
+ */
+
+function checkLists(childNodes, number) {
+ let count = 0;
+ // See comment above for matching requirements
+ for (let list of childNodes) {
+ if (list.isMailList && list.dirName.startsWith("TestList")) {
+ Assert.equal(list.URI, `${kPABData.URI}/${list.UID}`);
+ count++;
+ }
+ }
+
+ Assert.equal(count, number);
+}
+
+function run_test() {
+ loadABFile("../../../data/abLists1", kPABData.fileName);
+
+ // Test - Get the directory.
+
+ // XXX Getting all directories ensures we create all ABs because mailing
+ // lists need help initialising themselves
+ MailServices.ab.directories;
+
+ let AB = MailServices.ab.getDirectory(kPABData.URI);
+
+ // Test - Check all the expected mailing lists exist.
+
+ // There are three lists in abLists.mab by default.
+ checkLists(AB.childNodes, 3);
+
+ // Test - Add a new list.
+
+ var mailList = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+
+ mailList.isMailList = true;
+ mailList.dirName = "TestList4";
+ mailList.listNickName = "test4";
+ mailList.description = "test4description";
+
+ AB.addMailList(mailList);
+
+ // check them
+ checkLists(AB.childNodes, 4);
+
+ // Test - Remove a list.
+
+ mailList = MailServices.ab.getDirectory(
+ kPABData.URI + "/46cf4cbf-5945-43e4-a822-30c2f2969db9"
+ );
+
+ AB.deleteDirectory(mailList);
+
+ // check them
+ checkLists(AB.childNodes, 3);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js
new file mode 100644
index 0000000000..8faf6e064a
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsAbAutoCompleteSearch
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+function acObserver() {}
+
+acObserver.prototype = {
+ _search: null,
+ _result: null,
+
+ onSearchResult(aSearch, aResult) {
+ this._search = aSearch;
+ this._result = aResult;
+ },
+};
+
+function run_test() {
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=mydomain"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+ let obsNews = new acObserver();
+ let obsFollowup = new acObserver();
+
+ // Set up an identity in the account manager with the default settings
+ let identity = MailServices.accounts.createIdentity();
+
+ // Initially disable autocomplete
+ identity.autocompleteToMyDomain = false;
+ identity.email = "myemail@foo.invalid";
+
+ // Set up autocomplete parameters
+ let params = JSON.stringify({ idKey: identity.key, type: "addr_to" });
+ let paramsNews = JSON.stringify({
+ idKey: identity.key,
+ type: "addr_newsgroups",
+ });
+ let paramsFollowup = JSON.stringify({
+ idKey: identity.key,
+ type: "addr_followup",
+ });
+
+ // Test - Valid search - this should return no results (autocomplete disabled)
+ acs.startSearch("test", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "test");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Now enable autocomplete for this identity
+ identity.autocompleteToMyDomain = true;
+
+ // Test - Search with empty string
+
+ acs.startSearch(null, params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, null);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ acs.startSearch("", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Test - Check ignoring result with comma
+
+ acs.startSearch("a,b", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "a,b");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Test - Check returning search string with @ sign
+
+ acs.startSearch("a@b", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "a@b");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 1);
+
+ Assert.equal(obs._result.getValueAt(0), "a@b");
+ Assert.equal(obs._result.getLabelAt(0), "a@b");
+ Assert.equal(obs._result.getCommentAt(0), null);
+ Assert.equal(obs._result.getStyleAt(0), "default-match");
+ Assert.equal(obs._result.getImageAt(0), null);
+
+ // No autocomplete for addr_newsgroups!
+ acs.startSearch("a@b", paramsNews, null, obsNews);
+ Assert.ok(obsNews._result == null || obsNews._result.matchCount == 0);
+
+ // No autocomplete for addr_followup!
+ acs.startSearch("a@b", paramsFollowup, null, obsFollowup);
+ Assert.ok(obsFollowup._result == null || obsFollowup._result.matchCount == 0);
+
+ // Test - Add default domain
+
+ acs.startSearch("test1", params, null, obs);
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "test1");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 1);
+
+ Assert.equal(obs._result.getValueAt(0), "test1@foo.invalid");
+ Assert.equal(obs._result.getLabelAt(0), "test1@foo.invalid");
+ Assert.equal(obs._result.getCommentAt(0), null);
+ Assert.equal(obs._result.getStyleAt(0), "default-match");
+ Assert.equal(obs._result.getImageAt(0), null);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
new file mode 100644
index 0000000000..ffe48506ce
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js
@@ -0,0 +1,468 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * First test suite for nsAbAutoCompleteSearch - tests searching in address
+ * books for autocomplete matches, and checks sort order is correct (without
+ * popularity checks).
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+// Input and results arrays for the autocomplete tests. This are potentially
+// more complicated than really required, but it was easier to do them
+// on a pattern rather just doing the odd spot check.
+//
+// Note the expected arrays are in expected sort order as well.
+var results = [
+ { email: "d <ema@foo.invalid>", dirName: kPABData.dirName }, // 0
+ { email: "di <emai@foo.invalid>", dirName: kPABData.dirName }, // 1
+ { email: "dis <email@foo.invalid>", dirName: kPABData.dirName }, // 2
+ { email: "disp <e@foo.invalid>", dirName: kPABData.dirName }, // 3
+ { email: "displ <em@foo.invalid>", dirName: kPABData.dirName }, // 4
+ {
+ email: "DisplayName1 <PrimaryEmail1@test.invalid>", // 5
+ dirName: kCABData.dirName,
+ },
+ { email: "t <list>", dirName: kPABData.dirName }, // 6
+ { email: "te <lis>", dirName: kPABData.dirName }, // 7
+ { email: "tes <li>", dirName: kPABData.dirName }, // 8
+ // this contact has a nickname of "abcdef"
+ { email: "test <l>", dirName: kPABData.dirName }, // 9
+ { email: "doh, james <DohJames@foo.invalid>", dirName: kPABData.dirName }, // 10
+];
+var firstNames = [
+ { search: "f", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "fi", expected: [0, 1, 3, 4, 5] },
+ { search: "fir", expected: [0, 1, 4, 5] },
+ { search: "firs", expected: [0, 1, 5] },
+ { search: "first", expected: [1, 5] },
+ { search: "firstn", expected: [5] },
+];
+
+var lastNames = [
+ { search: "l", expected: [6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 10] },
+ { search: "la", expected: [0, 2, 3, 4, 5] },
+ { search: "las", expected: [0, 3, 4, 5] },
+ { search: "last", expected: [0, 4, 5] },
+ { search: "lastn", expected: [0, 5] },
+ { search: "lastna", expected: [5] },
+];
+
+var displayNames = [
+ { search: "d", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "di", expected: [1, 2, 3, 4, 5] },
+ { search: "dis", expected: [2, 3, 4, 5] },
+ { search: "disp", expected: [3, 4, 5] },
+ { search: "displ", expected: [4, 5] },
+ { search: "displa", expected: [5] },
+ { search: "doh,", expected: [10] },
+];
+
+var nickNames = [
+ { search: "n", expected: [4, 0, 1, 2, 3, 5, 10] },
+ { search: "ni", expected: [0, 1, 2, 3, 5] },
+ { search: "nic", expected: [1, 2, 3, 5] },
+ { search: "nick", expected: [2, 3, 5] },
+ { search: "nickn", expected: [3, 5] },
+ { search: "nickna", expected: [5] },
+];
+
+var emails = [
+ { search: "e", expected: [0, 1, 2, 3, 4, 5, 10, 7, 8, 9] },
+ { search: "em", expected: [0, 1, 2, 4, 5] },
+ { search: "ema", expected: [0, 1, 2, 5] },
+ { search: "emai", expected: [1, 2, 5] },
+ { search: "email", expected: [2, 5] },
+];
+
+// "l" case tested above
+var lists = [
+ { search: "li", expected: [6, 7, 8, 0, 1, 2, 3, 4, 5, 10] },
+ { search: "lis", expected: [6, 7] },
+ { search: "list", expected: [6] },
+ { search: "t", expected: [6, 7, 8, 9, 5, 0, 1, 4] },
+ { search: "te", expected: [7, 8, 9, 5] },
+ { search: "tes", expected: [8, 9, 5] },
+ { search: "test", expected: [9, 5] },
+ { search: "abcdef", expected: [9] }, // Bug 441586
+];
+
+var bothNames = [
+ { search: "f l", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "l f", expected: [0, 1, 2, 3, 4, 5, 10, 9] },
+ { search: "firstn lastna", expected: [5] },
+ { search: "lastna firstna", expected: [5] },
+];
+
+var inputs = [
+ firstNames,
+ lastNames,
+ displayNames,
+ nickNames,
+ emails,
+ lists,
+ bothNames,
+];
+
+var PAB_CARD_DATA = [
+ {
+ FirstName: "firs",
+ LastName: "lastn",
+ DisplayName: "d",
+ NickName: "ni",
+ PrimaryEmail: "ema@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "first",
+ LastName: "l",
+ DisplayName: "di",
+ NickName: "nic",
+ PrimaryEmail: "emai@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "f",
+ LastName: "la",
+ DisplayName: "dis",
+ NickName: "nick",
+ PrimaryEmail: "email@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "fi",
+ LastName: "las",
+ DisplayName: "disp",
+ NickName: "nickn",
+ PrimaryEmail: "e@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "fir",
+ LastName: "last",
+ DisplayName: "displ",
+ NickName: "n",
+ PrimaryEmail: "em@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "Doh",
+ LastName: "James",
+ DisplayName: "doh, james",
+ NickName: "j",
+ PrimaryEmail: "DohJames@foo.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+];
+
+var PAB_LIST_DATA = [
+ {
+ dirName: "t",
+ listNickName: null,
+ description: "list",
+ },
+ {
+ dirName: "te",
+ listNickName: null,
+ description: "lis",
+ },
+ {
+ dirName: "tes",
+ listNickName: null,
+ description: "li",
+ },
+ {
+ dirName: "test",
+ listNickName: "abcdef",
+ description: "l",
+ },
+];
+
+var CAB_CARD_DATA = [
+ {
+ FirstName: "FirstName1",
+ LastName: "LastName1",
+ DisplayName: "DisplayName1",
+ NickName: "NickName1",
+ PrimaryEmail: "PrimaryEmail1@test.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+ {
+ FirstName: "Empty",
+ LastName: "Email",
+ DisplayName: "Empty Email",
+ PreferDisplayName: true,
+ PopularityIndex: 0,
+ },
+];
+
+var CAB_LIST_DATA = [];
+
+function setupAddressBookData(aDirURI, aCardData, aMailListData) {
+ let ab = MailServices.ab.getDirectory(aDirURI);
+
+ // Getting all directories ensures we create all ABs because mailing
+ // lists need help initialising themselves
+ MailServices.ab.directories;
+
+ for (let card of ab.childCards) {
+ ab.dropCard(card, false);
+ }
+
+ aCardData.forEach(function (cd) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (var prop in cd) {
+ card.setProperty(prop, cd[prop]);
+ }
+ ab.addCard(card);
+ });
+
+ aMailListData.forEach(function (ld) {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ for (var prop in ld) {
+ list[prop] = ld[prop];
+ }
+ ab.addMailList(list);
+ });
+}
+
+add_task(async () => {
+ // Set up addresses for in the personal address book.
+ setupAddressBookData(kPABData.URI, PAB_CARD_DATA, PAB_LIST_DATA);
+ // ... and collected addresses address book.
+ setupAddressBookData(kCABData.URI, CAB_CARD_DATA, CAB_LIST_DATA);
+
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+ let obsNews = new acObserver();
+ let obsFollowup = new acObserver();
+
+ // Test - Check disabling of autocomplete
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", false);
+
+ let param = JSON.stringify({ type: "addr_to" });
+ let paramNews = JSON.stringify({ type: "addr_newsgroups" });
+ let paramFollowup = JSON.stringify({ type: "addr_followup" });
+
+ let resultPromise = obs.waitForResult();
+ acs.startSearch("abc", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "abc");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+
+ // Test - Check Enabling of autocomplete, but with empty string.
+
+ Services.prefs.setBoolPref("mail.enable_autocomplete", true);
+
+ resultPromise = obs.waitForResult();
+ acs.startSearch(null, param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, null);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_IGNORED);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+ Assert.equal(obs._result.defaultIndex, -1);
+
+ // Test - No matches
+
+ resultPromise = obs.waitForResult();
+ acs.startSearch("asjdkljdgfjglkfg", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "asjdkljdgfjglkfg");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 0);
+ Assert.equal(obs._result.defaultIndex, -1);
+
+ // Test - Matches
+
+ // Basic quick-check
+ resultPromise = obs.waitForResult();
+ acs.startSearch("email", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "email");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 2);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getCommentAt(0), "");
+ Assert.equal(obs._result.getStyleAt(0), "local-abook");
+ Assert.equal(obs._result.getImageAt(0), "");
+
+ // quick-check that nothing is found for addr_newsgroups
+ resultPromise = obsNews.waitForResult();
+ acs.startSearch("email", paramNews, null, obsNews);
+ await resultPromise;
+ Assert.ok(obsNews._result == null || obsNews._result.matchCount == 0);
+
+ // quick-check that nothing is found for addr_followup
+ resultPromise = obsFollowup.waitForResult();
+ acs.startSearch("a@b", paramFollowup, null, obsFollowup);
+ await resultPromise;
+ Assert.ok(obsFollowup._result == null || obsFollowup._result.matchCount == 0);
+
+ // Now quick-check with the address book name in the comment column.
+ Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
+
+ resultPromise = obs.waitForResult();
+ acs.startSearch("email", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "email");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 2);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getCommentAt(0), kPABData.dirName);
+ Assert.equal(obs._result.getStyleAt(0), "local-abook");
+ Assert.equal(obs._result.getImageAt(0), "");
+
+ // Check input with different case
+ resultPromise = obs.waitForResult();
+ acs.startSearch("EMAIL", param, null, obs);
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, "EMAIL");
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, 2);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>");
+ Assert.equal(obs._result.getCommentAt(0), kPABData.dirName);
+ Assert.equal(obs._result.getStyleAt(0), "local-abook");
+ Assert.equal(obs._result.getImageAt(0), "");
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ let prevRes = obs._result;
+ print("Search #" + index + ": search=" + element.search);
+ resultPromise = obs.waitForResult();
+ acs.startSearch(element.search, param, prevRes, obs);
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (result " +
+ element.expected[i] +
+ "): " +
+ results[element.expected[i]].email
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getCommentAt(i),
+ results[element.expected[i]].dirName
+ );
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+
+ // Test - Popularity Index
+ print("Checking by popularity index:");
+ let pab = MailServices.ab.getDirectory(kPABData.URI);
+
+ for (let card of pab.childCards) {
+ if (card.isMailList) {
+ continue;
+ }
+
+ switch (card.displayName) {
+ case "dis": // 2
+ case "disp": // 3
+ card.setProperty("PopularityIndex", 4);
+ break;
+ case "displ": // 4
+ card.setProperty("PopularityIndex", 5);
+ break;
+ case "d": // 0
+ card.setProperty("PopularityIndex", 1);
+ break;
+ case "di": // 1
+ card.setProperty("PopularityIndex", 20);
+ break;
+ default:
+ break;
+ }
+
+ pab.modifyCard(card);
+ }
+
+ const popularitySearch = [
+ { search: "d", expected: [1, 4, 2, 3, 0, 5, 10, 9] },
+ { search: "di", expected: [1, 4, 2, 3, 5] },
+ { search: "dis", expected: [4, 2, 3, 5] },
+ { search: "disp", expected: [4, 3, 5] },
+ { search: "displ", expected: [4, 5] },
+ { search: "displa", expected: [5] },
+ ];
+
+ for (let i = 0; i < popularitySearch.length; i++) {
+ await checkInputItem(popularitySearch[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js
new file mode 100644
index 0000000000..b2dafd41e8
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Second Test suite for nsAbAutoCompleteSearch - test follow-on lookup after
+ * a previous search.
+ *
+ * We run this test without address books, constructing manually ourselves,
+ * so that we can ensure that we're not getting the data out of the address
+ * books.
+ */
+
+var { getModelQuery } = ChromeUtils.import(
+ "resource:///modules/ABQueryUtils.jsm"
+);
+
+// taken from nsAbAutoCompleteSearch.js
+var ACR = Ci.nsIAutoCompleteResult;
+var nsIAbAutoCompleteResult = Ci.nsIAbAutoCompleteResult;
+
+function nsAbAutoCompleteResult(aSearchString) {
+ // Can't create this in the prototype as we'd get the same array for
+ // all instances
+ this._searchResults = [];
+ this.searchString = aSearchString;
+ this.modelQuery = getModelQuery("mail.addr_book.autocompletequery.format");
+ this.asyncDirectories = [];
+}
+
+nsAbAutoCompleteResult.prototype = {
+ _searchResults: null,
+
+ // nsIAutoCompleteResult
+
+ modelQuery: null,
+ searchString: null,
+ searchResult: ACR.RESULT_NOMATCH,
+ defaultIndex: -1,
+ errorDescription: null,
+
+ get matchCount() {
+ return this._searchResults.length;
+ },
+
+ getValueAt: function getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getLabelAt: function getLabelAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getCommentAt: function getCommentAt(aIndex) {
+ return this._searchResults[aIndex].comment;
+ },
+
+ getStyleAt: function getStyleAt(aIndex) {
+ return "local-abook";
+ },
+
+ getImageAt: function getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt: function removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsIAbAutoCompleteResult
+
+ getCardAt: function getCardAt(aIndex) {
+ return this._searchResults[aIndex].card;
+ },
+
+ getEmailToUse: function getEmailToUse(aIndex) {
+ // For this test we can just use the primary email here.
+ return this._searchResults[aIndex].card.primaryEmail;
+ },
+
+ isCompleteResult: function isCompleteResult(aIndex) {
+ // For this test we claim all results are complete.
+ return true;
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIAutoCompleteResult",
+ "nsIAbAutoCompleteResult",
+ ]),
+};
+
+function createCard(chars, popularity) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.firstName = "firstName".slice(0, chars);
+ card.lastName = "lastName".slice(0, chars);
+ card.displayName = "displayName".slice(0, chars);
+ card.primaryEmail = "email".slice(0, chars) + "@foo.invalid";
+ card.setProperty("NickName", "nickName".slice(0, chars));
+
+ return card;
+}
+
+var results = [
+ { email: "d <e@foo.invalid>", dirName: kPABData.dirName },
+ { email: "di <em@foo.invalid>", dirName: kPABData.dirName },
+ { email: "dis <ema@foo.invalid>", dirName: kPABData.dirName },
+];
+
+var firstNames = [
+ { search: "fi", expected: [1, 2] },
+ { search: "fir", expected: [2] },
+];
+
+var lastNames = [
+ { search: "la", expected: [1, 2] },
+ { search: "las", expected: [2] },
+];
+
+var inputs = [firstNames, lastNames];
+
+add_task(async () => {
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ // Ensure we've got the comment column set up for extra checking.
+ Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
+
+ // Make up the last autocomplete result
+ var lastResult = new nsAbAutoCompleteResult();
+
+ lastResult.searchString = "";
+ lastResult.searchResult = ACR.RESULT_SUCCESS;
+ lastResult.defaultIndex = 0;
+ lastResult.errorDescription = null;
+ for (let i = 0; i < results.length; ++i) {
+ lastResult._searchResults.push({
+ value: results[i].email,
+ comment: results[i].dirName,
+ card: createCard(i + 1, 0),
+ });
+ }
+
+ // Test - Matches
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to", idKey: "" }),
+ lastResult,
+ obs
+ );
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getCommentAt(i),
+ results[element.expected[i]].dirName
+ );
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
new file mode 100644
index 0000000000..4916c30bc5
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Third Test suite for nsAbAutoCompleteSearch - test for duplicate elimination
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var cards = [
+ {
+ email: "test@foo.invalid",
+ displayName: "",
+ popularityIndex: 0,
+ firstName: "test0",
+ value: "test@foo.invalid",
+ },
+ {
+ email: "test@foo.invalid",
+ displayName: "",
+ popularityIndex: 1,
+ firstName: "test1",
+ value: "test@foo.invalid",
+ },
+ {
+ email: "abc@foo.invalid",
+ displayName: "",
+ popularityIndex: 1,
+ firstName: "test2",
+ value: "abc@foo.invalid",
+ },
+ {
+ email: "foo1@foo.invalid",
+ displayName: "d",
+ popularityIndex: 0,
+ firstName: "first1",
+ value: "d <foo1@foo.invalid>",
+ },
+ {
+ email: "foo2@foo.invalid",
+ displayName: "di",
+ popularityIndex: 1,
+ firstName: "first1",
+ value: "di <foo2@foo.invalid>",
+ },
+ {
+ email: "foo3@foo.invalid",
+ displayName: "dis",
+ popularityIndex: 2,
+ firstName: "first2",
+ value: "dis <foo3@foo.invalid>",
+ },
+ {
+ email: "foo2@foo.invalid",
+ displayName: "di",
+ popularityIndex: 3,
+ firstName: "first2",
+ value: "di <foo2@foo.invalid>",
+ },
+ // this just tests we can search for the special chars '(' and ')', bug 749097
+ {
+ email: "bracket@not.invalid",
+ secondEmail: "h@not.invalid",
+ firstName: "Mr.",
+ displayName: "Mr. (Bracket)",
+ value: "Mr. (Bracket) <bracket@not.invalid>",
+ popularityIndex: 2,
+ },
+ {
+ email: "mr@(bracket).not.invalid",
+ secondEmail: "bracket@not.invalid",
+ firstName: "Mr.",
+ displayName: "Mr. Bracket",
+ value: "Mr. Bracket <mr@(bracket).not.invalid>",
+ popularityIndex: 1,
+ },
+];
+
+var duplicates = [
+ { search: "test", expected: [1, 2] },
+ { search: "first", expected: [6, 5, 3] },
+ { search: "(bracket)", expected: [7, 8] },
+];
+
+add_task(async () => {
+ // We set up the cards for this test manually as it is easier to set the
+ // popularity index and we don't need many.
+
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ card.displayName = element.displayName;
+ card.setProperty("PopularityIndex", element.popularityIndex);
+ card.firstName = element.firstName;
+
+ ab.addCard(card);
+ }
+
+ cards.forEach(createAndAddCard);
+
+ // Test - duplicate elements
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ async function checkInputItem(element, index) {
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (card " +
+ element.expected[i] +
+ "): " +
+ cards[element.expected[i]].value
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(obs._result.getValueAt(i), cards[element.expected[i]].value);
+ Assert.equal(obs._result.getLabelAt(i), cards[element.expected[i]].value);
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ Assert.equal(
+ obs._result.getCardAt(i).firstName,
+ cards[element.expected[i]].firstName
+ );
+ }
+ }
+
+ for (let i = 0; i < duplicates.length; i++) {
+ await checkInputItem(duplicates[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
new file mode 100644
index 0000000000..e1de6f1bbd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js
@@ -0,0 +1,258 @@
+/*
+ * Fourth Test suite for nsAbAutoCompleteSearch - test for second email address.
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var cards = [
+ // Basic tests for primary and secondary emails.
+ {
+ email: "primary@test.invalid",
+ secondEmail: "second@test.invalid",
+ firstName: "",
+ },
+ {
+ email: "test1@test.invalid",
+ secondEmail: "test2@test.invalid",
+ firstName: "firstName",
+ },
+ {
+ email: "bar1@test.invalid",
+ secondEmail: "bar2@test.invalid",
+ firstName: "sweet",
+ },
+ {
+ email: "boo1@test.invalid",
+ secondEmail: "boo2@test.invalid",
+ firstName: "sample",
+ },
+ {
+ email: "name@test.invalid",
+ secondEmail: "thename@test.invalid",
+ firstName: "thename",
+ },
+ // Test to check correct sorting of primary and secondary emails.
+ {
+ email: "foo_b@test.invalid",
+ secondEmail: "foo_a@test.invalid",
+ displayName: "sortbasic",
+ },
+ {
+ email: "d@test.invalid",
+ secondEmail: "e@test.invalid",
+ displayName: "testsort",
+ },
+ {
+ email: "c@test.invalid",
+ secondEmail: "a@test.invalid",
+ displayName: "testsort",
+ },
+ // "2testsort" does the same as "testsort" but turns the cards around to
+ // ensure the order is always consistent.
+ {
+ email: "c@test.invalid",
+ secondEmail: "a@test.invalid",
+ displayName: "2testsort",
+ },
+ {
+ email: "d@test.invalid",
+ secondEmail: "e@test.invalid",
+ displayName: "2testsort",
+ },
+ {
+ email: "g@test.invalid",
+ secondEmail: "f@test.invalid",
+ displayName: "3testsort",
+ popularityIndex: 3,
+ },
+ {
+ email: "j@test.invalid",
+ secondEmail: "h@test.invalid",
+ displayName: "3testsort",
+ popularityIndex: 5,
+ },
+ // Add a contact that matches, but has no email. Should not show up.
+ { displayName: "primaryX" },
+];
+
+// These are for the initial search
+var searches = [
+ "primary",
+ "second",
+ "firstName",
+ "thename",
+ "sortbasic",
+ "testsort",
+ "2testsort",
+ "3testsort",
+];
+
+var expectedResults = [
+ ["primary@test.invalid", "second@test.invalid"], // searching for primary/second returns
+ [
+ "second@test.invalid", // both the emails as the new search query
+ "primary@test.invalid",
+ ], // looks in both the fields.
+ ["test1@test.invalid", "test2@test.invalid"],
+ ["thename@test.invalid", "name@test.invalid"],
+ ["sortbasic <foo_b@test.invalid>", "sortbasic <foo_a@test.invalid>"],
+ [
+ "testsort <c@test.invalid>",
+ "testsort <a@test.invalid>",
+ "testsort <d@test.invalid>",
+ "testsort <e@test.invalid>",
+ "3testsort <j@test.invalid>",
+ "3testsort <h@test.invalid>",
+ "3testsort <g@test.invalid>",
+ "3testsort <f@test.invalid>",
+ "2testsort <c@test.invalid>",
+ "2testsort <a@test.invalid>",
+ "2testsort <d@test.invalid>",
+ "2testsort <e@test.invalid>",
+ ],
+ [
+ "2testsort <c@test.invalid>",
+ "2testsort <a@test.invalid>",
+ "2testsort <d@test.invalid>",
+ "2testsort <e@test.invalid>",
+ ],
+ [
+ "3testsort <j@test.invalid>",
+ "3testsort <h@test.invalid>",
+ "3testsort <g@test.invalid>",
+ "3testsort <f@test.invalid>",
+ ],
+];
+
+// These are for subsequent searches - reducing the number of results.
+var reductionSearches = ["b", "bo", "boo2"];
+
+var reductionExpectedResults = [
+ [
+ "bar1@test.invalid",
+ "bar2@test.invalid",
+ "boo1@test.invalid",
+ "boo2@test.invalid",
+ "sortbasic <foo_b@test.invalid>",
+ "sortbasic <foo_a@test.invalid>",
+ ],
+ ["boo1@test.invalid", "boo2@test.invalid"],
+ ["boo2@test.invalid"],
+];
+
+add_task(async () => {
+ // We set up the cards for this test manually as it is easier to set the
+ // popularity index and we don't need many.
+
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ if ("secondEmail" in element) {
+ card.setProperty("SecondEmail", element.secondEmail);
+ }
+ card.displayName = element.displayName;
+ if ("popularityIndex" in element) {
+ card.setProperty("PopularityIndex", element.popularityIndex);
+ }
+ card.firstName = element.firstName;
+
+ ab.addCard(card);
+ }
+
+ cards.forEach(createAndAddCard);
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ print("Checking Initial Searches");
+
+ async function checkSearch(element, index) {
+ print("Search #" + index + ": search=" + element);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element,
+ JSON.stringify({ type: "addr_to", idKey: "" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, expectedResults[index].length);
+
+ for (let i = 0; i < expectedResults[index].length; ++i) {
+ Assert.equal(obs._result.getValueAt(i), expectedResults[index][i]);
+ Assert.equal(obs._result.getLabelAt(i), expectedResults[index][i]);
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ }
+ }
+
+ for (let i = 0; i < searches.length; i++) {
+ await checkSearch(searches[i], i);
+ }
+
+ print("Checking Reduction of Search Results");
+
+ var lastResult = null;
+
+ async function checkReductionSearch(element, index) {
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element,
+ JSON.stringify({ type: "addr_to", idKey: "" }),
+ lastResult,
+ obs
+ );
+ await resultPromise;
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(
+ obs._result.matchCount,
+ reductionExpectedResults[index].length
+ );
+
+ for (var i = 0; i < reductionExpectedResults[index].length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ reductionExpectedResults[index][i]
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ reductionExpectedResults[index][i]
+ );
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ }
+ lastResult = obs._result;
+ }
+
+ for (let i = 0; i < reductionSearches.length; i++) {
+ await checkReductionSearch(reductionSearches[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
new file mode 100644
index 0000000000..a10ac5e4b4
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * This suite ensures that we can correctly read and re-set the popularity
+ * indexes on a
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var results = [
+ { email: "d <ema@test.invalid>", dirName: kPABData.dirName },
+ { email: "di <emai@test.invalid>", dirName: kPABData.dirName },
+ { email: "dis <email@test.invalid>", dirName: kPABData.dirName },
+ { email: "disp <e@test.invalid>", dirName: kPABData.dirName },
+ { email: "displ <em@test.invalid>", dirName: kPABData.dirName },
+ { email: "t <list>", dirName: kPABData.dirName },
+ { email: "te <lis>", dirName: kPABData.dirName },
+ { email: "tes <li>", dirName: kPABData.dirName },
+ // this contact has a nickname of "abcdef"
+ { email: "test <l>", dirName: kPABData.dirName },
+];
+
+var firstNames = [
+ { search: "f", expected: [4, 0, 1, 2, 3, 8] },
+ { search: "fi", expected: [4, 0, 1, 3] },
+ { search: "fir", expected: [4, 0, 1] },
+ { search: "firs", expected: [0, 1] },
+ { search: "first", expected: [1] },
+];
+
+var lastNames = [
+ { search: "l", expected: [5, 6, 7, 8, 4, 0, 1, 2, 3] },
+ { search: "la", expected: [4, 0, 2, 3] },
+ { search: "las", expected: [4, 0, 3] },
+ { search: "last", expected: [4, 0] },
+ { search: "lastn", expected: [0] },
+];
+
+var inputs = [firstNames, lastNames];
+
+add_task(async () => {
+ loadABFile("../../../data/tb2hexpopularity", kPABData.fileName);
+
+ // Test - Create a new search component
+
+ let acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ let obs = new acObserver();
+
+ // Ensure we've got the comment column set up for extra checking.
+ Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1);
+
+ // Test - Matches
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (card " +
+ element.expected[i] +
+ "): " +
+ results[element.expected[i]].email
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getCommentAt(i),
+ results[element.expected[i]].dirName
+ );
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+
+ // Card at result number 4 is the one with the TB 2 popularity set as "a"
+ // in the file, so check that we're now setting the popularity to 10
+ // and hence future tests don't have to convert it.
+ if (element.expected[i] == 4) {
+ let result = obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult);
+ Assert.equal(
+ result.getCardAt(i).getProperty("PopularityIndex", -1),
+ 10
+ );
+ }
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
new file mode 100644
index 0000000000..08b38de7c3
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js
@@ -0,0 +1,248 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Tests for for nsAbAutoCompleteSearch scoring.
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+var cards = [
+ {
+ // 0
+ email: "jd.who@example.com",
+ displayName: "John Doe (:xx)",
+ popularityIndex: 0,
+ firstName: "John",
+ value: "John Doe (:xx) <jd.who@example.com>",
+ },
+
+ {
+ // 1
+ email: "janey_who@example.com",
+ displayName: "Jane Doe",
+ popularityIndex: 0,
+ value: "Jane Doe <janey_who@example.com>",
+ },
+
+ {
+ // 2
+ email: "pf@example.com",
+ displayName: 'Paul "Shitbreak" Finch',
+ popularityIndex: 0,
+ value: 'Paul "Shitbreak" Finch <pf@example.com>',
+ },
+
+ {
+ // 3
+ email: "js@example.com",
+ displayName: "Janine (Stifflers Mom)",
+ popularityIndex: 0,
+ value: "Janine (Stifflers Mom) <js@example.com>",
+ },
+
+ {
+ // 4
+ email: "ex0@example.com",
+ displayName: "Ajden",
+ popularityIndex: 0,
+ value: "Ajden <ex0@example.com>",
+ },
+
+ {
+ // 5
+ email: "5@example.com",
+ displayName: "Foxx",
+ popularityIndex: 0,
+ value: "Foxx <5@example.com>",
+ },
+
+ {
+ // 6
+ email: "6@example.com",
+ displayName: "thewho",
+ popularityIndex: 0,
+ value: "thewho <6@example.com>",
+ },
+
+ {
+ // 7
+ email: "7@example.com",
+ displayName: "fakeshit",
+ popularityIndex: 0,
+ value: "fakeshit <7@example.com>",
+ },
+
+ {
+ // 8
+ email: "8@example.com",
+ displayName: "mastiff",
+ popularityIndex: 0,
+ value: "mastiff <8@example.com>",
+ },
+
+ {
+ // 9
+ email: "9@example.com",
+ displayName: "anyjohn",
+ popularityIndex: 0,
+ value: "anyjohn <9@example.com>",
+ },
+
+ {
+ // 10
+ email: "10@example.com",
+ displayName: "däsh l18n",
+ popularityIndex: 0,
+ value: "däsh l18n <10@example.com>",
+ },
+
+ {
+ // 11
+ email: "11@example.com",
+ displayName: "paul mary",
+ popularityIndex: 0,
+ firstName: "paul",
+ lastName: "mary meyer",
+ value: "paul mary <11@example.com>",
+ },
+
+ {
+ // 12
+ email: "12@example.com",
+ displayName: "paul meyer",
+ popularityIndex: 0,
+ firstName: "paul",
+ lastName: "mary meyer",
+ value: "paul meyer <12@example.com>",
+ },
+
+ {
+ // 13
+ email: "13@example.com",
+ displayName: "mr iron man (exp dev)",
+ popularityIndex: 0,
+ firstName: "iron",
+ lastName: "man",
+ value: "mr iron man (exp dev) <13@example.com>",
+ },
+
+ {
+ // 14
+ email: "14@example.com",
+ displayName: "michael",
+ popularityIndex: 0,
+ nickName: "short",
+ value: "michael <14@example.com>",
+ },
+
+ {
+ // 15
+ email: "15@example.com",
+ displayName: "good boy",
+ popularityIndex: 0,
+ nickName: "sh",
+ value: "good boy <15@example.com>",
+ },
+
+ {
+ // 16
+ email: "16@example.com",
+ displayName: "sherlock holmes",
+ popularityIndex: 0,
+ value: "sherlock holmes <16@example.com>",
+ },
+];
+
+var inputs = [
+ { search: "john", expected: [0, 9] },
+ { search: "doe", expected: [1, 0] },
+ { search: "jd", expected: [0, 4] },
+ { search: "who", expected: [1, 0, 6] },
+ { search: "xx", expected: [0, 5] },
+ { search: "jan", expected: [1, 3] },
+ // expecting nickname to score highest.
+ { search: "sh", expected: [15, 14, 2, 16, 10, 7] },
+ { search: "st", expected: [3, 8] },
+ { search: "paul mary", expected: [11, 12] },
+ { search: '"paul mary"', expected: [11] },
+ { search: '"iron man" mr "exp dev"', expected: [13] },
+ { search: "short", expected: [14] },
+];
+
+add_task(async () => {
+ // We set up the cards for this test manually as it is easier to set the
+ // popularity index and we don't need many.
+
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ card.displayName = element.displayName;
+ card.setProperty("PopularityIndex", element.popularityIndex);
+ card.firstName = element.firstName;
+ card.lastName = element.lastName;
+ if ("nickName" in element) {
+ card.setProperty("NickName", element.nickName);
+ }
+
+ ab.addCard(card);
+ }
+
+ cards.forEach(createAndAddCard);
+
+ // Test - duplicate elements
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ async function checkInputItem(element, index) {
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(
+ element.search,
+ JSON.stringify({ type: "addr_to" }),
+ null,
+ obs
+ );
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (card " +
+ element.expected[i] +
+ "): " +
+ cards[element.expected[i]].value
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(obs._result.getValueAt(i), cards[element.expected[i]].value);
+ Assert.equal(obs._result.getLabelAt(i), cards[element.expected[i]].value);
+ }
+ }
+
+ for (let i = 0; i < inputs.length; i++) {
+ await checkInputItem(inputs[i], i);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
new file mode 100644
index 0000000000..28bd2d1836
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests for nsAbAutoCompleteSearch - tests searching in address
+ * books for autocomplete matches, and checks sort order is correct
+ * according to scores.
+ */
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+// Input and results arrays for the autocomplete tests.
+
+// Note the expected arrays are in expected sort order as well.
+
+var results = [
+ { email: "Tomas Doe <tomez.doe@foo.invalid>" }, // 0
+ { email: "Tomas Doe <tomez.doe@foo2.invalid>" }, // 1
+ { email: "Tomas Doe <tomez.doe@b.example.com>" }, // 2
+ { email: "Tomas Doe <tomez.doe@a.example.com>" }, // 3
+ { email: "Tomek Smith <tomek@example.com>" }, // 4
+];
+
+var inputs = [
+ [
+ { search: "t", expected: [2, 3, 0, 1, 4] },
+ { search: "tom", expected: [0, 1, 2, 3, 4] },
+ { search: "tomek", expected: [4] },
+ ],
+];
+
+var PAB_CARD_DATA = [
+ {
+ FirstName: "Tomas",
+ LastName: "Doe",
+ DisplayName: "Tomas Doe",
+ NickName: "tom",
+ PrimaryEmail: "tomez.doe@foo.invalid",
+ SecondEmail: "tomez.doe@foo2.invalid",
+ PreferDisplayName: true,
+ PopularityIndex: 10,
+ // Poison the card data with an unparseable birthday. This will cause the
+ // vCard parser to throw an exception, but it should be caught and the
+ // search should carry on as normal.
+ BirthDay: 25,
+ BirthMonth: 9,
+ BirthYear: "NaN",
+ },
+ {
+ FirstName: "Tomas",
+ LastName: "Doe",
+ DisplayName: "Tomas Doe",
+ PrimaryEmail: "tomez.doe@b.example.com",
+ SecondEmail: "tomez.doe@a.example.com",
+ PreferDisplayName: true,
+ PopularityIndex: 200,
+ },
+ {
+ FirstName: "Tomek",
+ LastName: "Smith",
+ DisplayName: "Tomek Smith",
+ PrimaryEmail: "tomek@example.com",
+ PreferDisplayName: true,
+ PopularityIndex: 3,
+ },
+];
+
+function setupAddressBookData(aDirURI, aCardData, aMailListData) {
+ let ab = MailServices.ab.getDirectory(aDirURI);
+
+ // Getting all directories ensures we create all ABs because mailing
+ // lists need help initialising themselves
+ MailServices.ab.directories;
+
+ for (let card of ab.childCards) {
+ ab.dropCard(card, false);
+ }
+
+ aCardData.forEach(function (cd) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (var prop in cd) {
+ card.setProperty(prop, cd[prop]);
+ }
+ ab.addCard(card);
+ });
+
+ aMailListData.forEach(function (ld) {
+ let list = Cc[
+ "@mozilla.org/addressbook/directoryproperty;1"
+ ].createInstance(Ci.nsIAbDirectory);
+ list.isMailList = true;
+ for (var prop in ld) {
+ list[prop] = ld[prop];
+ }
+ ab.addMailList(list);
+ });
+}
+
+add_task(async () => {
+ // Set up addresses for in the personal address book.
+ setupAddressBookData(kPABData.URI, PAB_CARD_DATA, []);
+
+ // Test - Create a new search component
+
+ var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+
+ var obs = new acObserver();
+
+ let param = JSON.stringify({ type: "addr_to" });
+
+ // Now check multiple matches
+ async function checkInputItem(element, index) {
+ let prevRes = obs._result;
+ print("Search #" + index + ": search=" + element.search);
+ let resultPromise = obs.waitForResult();
+ acs.startSearch(element.search, param, prevRes, obs);
+ await resultPromise;
+
+ for (let i = 0; i < obs._result.matchCount; i++) {
+ print("... got " + i + ": " + obs._result.getValueAt(i));
+ }
+ for (let i = 0; i < element.expected.length; i++) {
+ print(
+ "... expected " +
+ i +
+ " (result " +
+ element.expected[i] +
+ "): " +
+ results[element.expected[i]].email
+ );
+ }
+
+ Assert.equal(obs._search, acs);
+ Assert.equal(obs._result.searchString, element.search);
+ Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS);
+ Assert.equal(obs._result.errorDescription, null);
+ Assert.equal(obs._result.matchCount, element.expected.length);
+ Assert.equal(obs._result.defaultIndex, 0);
+
+ for (let i = 0; i < element.expected.length; ++i) {
+ Assert.equal(
+ obs._result.getValueAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(
+ obs._result.getLabelAt(i),
+ results[element.expected[i]].email
+ );
+ Assert.equal(obs._result.getCommentAt(i), "");
+ Assert.equal(obs._result.getStyleAt(i), "local-abook");
+ Assert.equal(obs._result.getImageAt(i), "");
+ }
+ }
+
+ for (let inputSet of inputs) {
+ for (let i = 0; i < inputSet.length; i++) {
+ await checkInputItem(inputSet[i], i);
+ }
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js
new file mode 100644
index 0000000000..37238c51e8
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsAbManager functions relating to add/delete directories and
+ * getting the list of directories..
+ */
+
+function checkDirs(aDirs, aDirArray) {
+ // Don't modify the passed in array.
+ var dirArray = aDirArray.concat();
+
+ for (let dir of aDirs) {
+ var loc = dirArray.indexOf(dir.URI);
+
+ Assert.equal(MailServices.ab.getDirectory(dir.URI), dir);
+
+ if (loc == -1) {
+ do_throw(
+ "Unexpected directory " + dir.URI + " found in address book list"
+ );
+ } else {
+ dirArray[loc] = null;
+ }
+ }
+
+ dirArray.forEach(function (value) {
+ Assert.equal(value, null);
+ });
+}
+
+function addDirectory(dirName) {
+ // Add the directory
+ let dirPrefId = MailServices.ab.newAddressBook(dirName, "", kPABData.dirType);
+ return MailServices.ab.getDirectoryFromId(dirPrefId);
+}
+
+async function run_test() {
+ var expectedABs = [kPABData.URI, kCABData.URI];
+
+ // Test - Check initial directories
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Add a directory
+
+ var newDirectory1 = addDirectory("testAb1");
+
+ // Test - Check new directory list
+ expectedABs.push(newDirectory1.URI);
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Repeat for a second directory
+
+ var newDirectory2 = addDirectory("testAb2");
+
+ // Test - Check new directory list
+ expectedABs.push(newDirectory2.URI);
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Remove a directory
+
+ var pos = expectedABs.indexOf(newDirectory1.URI);
+
+ expectedABs.splice(pos, 1);
+
+ await promiseDirectoryRemoved(newDirectory1.URI);
+ newDirectory1 = null;
+
+ // Test - Check new directory list
+
+ checkDirs(MailServices.ab.directories, expectedABs);
+
+ // Test - Repeat the removal
+
+ await promiseDirectoryRemoved(newDirectory2.URI);
+ newDirectory2 = null;
+
+ expectedABs.pop();
+
+ // Test - Check new directory list
+ checkDirs(MailServices.ab.directories, expectedABs);
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js
new file mode 100644
index 0000000000..851017a593
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+/**
+ * Tests that an address book, once renamed, is not deleted when a sibling address book is deleted.
+ */
+
+function addDirectory(dirName) {
+ let dirPrefId = MailServices.ab.newAddressBook(dirName, "", kPABData.dirType);
+ return MailServices.ab.getDirectoryFromId(dirPrefId);
+}
+
+function renameDirectory(directory, newName) {
+ directory.dirName = newName;
+}
+
+/**
+ * Create 4 addressbooks (directories). Rename the second one and delete
+ * the third one. Check if their names are still correct. (bug 745664)
+ */
+async function run_test() {
+ let dirNames = ["testAb0", "testAb1", "testAb2", "testAb3"];
+ let directories = [];
+
+ for (let dirName of dirNames) {
+ directories.push(addDirectory(dirName));
+ }
+
+ dirNames[1] = "newTestAb1";
+ renameDirectory(directories[1], dirNames[1]);
+ for (let dir in dirNames) {
+ Assert.equal(dirNames[dir], directories[dir].dirName);
+ }
+ await promiseDirectoryRemoved(directories[2].URI);
+ dirNames.splice(2, 1);
+ directories.splice(2, 1);
+
+ for (let dir in dirNames) {
+ Assert.equal(dirNames[dir], directories[dir].dirName);
+ }
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js
new file mode 100644
index 0000000000..9b9d5a124d
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js
@@ -0,0 +1,75 @@
+/* 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/. */
+
+/**
+ * Creating a new address book with the same name as an existing one should
+ * always produce a unique preference branch. Check that it does.
+ */
+add_task(function testSameName() {
+ let name0 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name0, "ldap_2.servers.name");
+
+ let name1 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name1, "ldap_2.servers.name_1");
+
+ let name2 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name2, "ldap_2.servers.name_2");
+
+ let name3 = MailServices.ab.newAddressBook("name", null, kPABData.dirType);
+ equal(name3, "ldap_2.servers.name_3");
+});
+
+/**
+ * Tests that creating a new book with the UID argument assigns the UID to
+ * that book and stores it in the preferences.
+ */
+function subtestCreateWithUID(type, uidValue) {
+ let prefID = MailServices.ab.newAddressBook(
+ "Got a UID",
+ null,
+ type,
+ uidValue
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`${prefID}.uid`, ""),
+ uidValue,
+ "UID is saved to the preferences"
+ );
+
+ let book = MailServices.ab.getDirectoryFromId(prefID);
+ Assert.equal(book.UID, uidValue, "created book has the right UID");
+}
+
+add_task(function testCreateWithUID_JS() {
+ subtestCreateWithUID(
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE,
+ "01234567-89ab-cdef-0123-456789abcdef"
+ );
+
+ Assert.throws(
+ () =>
+ MailServices.ab.newAddressBook(
+ "Should fail",
+ null,
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE,
+ "01234567-89ab-cdef-0123-456789abcdef"
+ ),
+ /NS_ERROR_ABORT/,
+ "reusing a UID should throw an exception"
+ );
+});
+
+add_task(function testCreateWithUID_CardDAV() {
+ subtestCreateWithUID(
+ Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE,
+ "456789ab-cdef-0123-4567-89abcdef0123"
+ );
+});
+
+add_task(function testCreateWithUID_LDAP() {
+ subtestCreateWithUID(
+ Ci.nsIAbManager.LDAP_DIRECTORY_TYPE,
+ "89abcdef-0123-4567-89ab-cdef01234567"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js
new file mode 100644
index 0000000000..42eb370f7b
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js
@@ -0,0 +1,43 @@
+/* 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";
+
+add_task(async function createAddressBook() {
+ Assert.ok(!MailServices.ab.getDirectoryFromUID("nonsense"));
+
+ let pabFromURI = MailServices.ab.getDirectory(kPABData.URI);
+ let pabFromId = MailServices.ab.getDirectoryFromId(kPABData.dirPrefID);
+ let pabFromUID = MailServices.ab.getDirectoryFromUID(pabFromURI.UID);
+
+ Assert.equal(pabFromId, pabFromURI);
+ Assert.equal(pabFromUID, pabFromURI);
+
+ let historyFromURI = MailServices.ab.getDirectory(kCABData.URI);
+ let historyFromId = MailServices.ab.getDirectoryFromId(kCABData.dirPrefID);
+ let historyFromUID = MailServices.ab.getDirectoryFromUID(historyFromURI.UID);
+
+ Assert.equal(historyFromId, historyFromURI);
+ Assert.equal(historyFromUID, historyFromURI);
+ Assert.notEqual(historyFromUID, pabFromUID);
+
+ let newPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ kPABData.dirType
+ );
+ let newFromId = MailServices.ab.getDirectoryFromId(newPrefId);
+
+ let newFromURI = MailServices.ab.getDirectory(newFromId.URI);
+ let newFromUID = MailServices.ab.getDirectoryFromUID(newFromId.UID);
+
+ Assert.equal(newFromId, newFromURI);
+ Assert.equal(newFromUID, newFromURI);
+ Assert.notEqual(newFromUID, pabFromUID);
+ Assert.notEqual(newFromUID, historyFromUID);
+
+ await promiseDirectoryRemoved(newFromId.URI);
+
+ Assert.ok(!MailServices.ab.getDirectoryFromUID(newFromId.UID));
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js
new file mode 100644
index 0000000000..05beb37a9e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js
@@ -0,0 +1,27 @@
+/* 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";
+
+/**
+ * Tests getMailListFromName() and mailListNameExists() which relies on it.
+ */
+add_task(function testGetMailListFromName() {
+ loadABFile("../../../data/abLists1", kPABData.fileName);
+
+ for (let listName of ["TestList1", "TestList2", "TestList3"]) {
+ Assert.ok(
+ MailServices.ab.mailListNameExists(listName),
+ `AddrBookManager has ${listName}`
+ );
+
+ let list = MailServices.ab.getMailListFromName(listName);
+ Assert.ok(list, `"${listName}" is not null`);
+ Assert.equal(
+ list.dirName,
+ listName,
+ `"${listName}" dirName is "${listName}"`
+ );
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js b/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js
new file mode 100644
index 0000000000..7d51cecee1
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+/**
+ * Test suite for the getMailListFromName() function.
+ */
+
+"use strict";
+
+/**
+ * Tests the getMailListFromName function returns the correct nsIAbDirectory,
+ * also tests the hasMailListWithName function as it uses the same code.
+ */
+add_task(function testGetMailListFromName() {
+ loadABFile("../../../data/abLists1", kPABData.fileName);
+
+ // Test all top level lists are returned.
+ let root = MailServices.ab.getDirectory(kPABData.URI);
+ for (let listName of ["TestList1", "TestList2", "TestList3"]) {
+ Assert.ok(root.hasMailListWithName(listName), `parent has "${listName}"`);
+
+ let list = root.getMailListFromName(listName);
+ Assert.ok(list, `"${listName}" is not null`);
+ Assert.equal(
+ list.dirName,
+ listName,
+ `"${listName}" dirName is "${listName}"`
+ );
+ }
+
+ Assert.ok(
+ !root.hasMailListWithName("Non existent"),
+ "hasMailListWithName() returns false for non-existent list name"
+ );
+ Assert.ok(
+ !root.getMailListFromName("Non existent"),
+ "getMailListFromName() returns null for non-existent list name"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js b/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js
new file mode 100644
index 0000000000..b24b9ca20e
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js
@@ -0,0 +1,428 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsLDAPURL functions.
+ */
+
+// If we are still using the wallet service, then default port numbers
+// are still visible in the password manager, and therefore we need to have
+// them in the url. The toolkit login manager doesn't do this.
+const usingWallet = "nsIWalletService" in Ci;
+const portAdpt = usingWallet ? ":389" : "";
+
+const ldapURLs = [
+ {
+ url: "ldap://localhost/dc=test",
+ spec: "ldap://localhost/dc=test",
+ asciiSpec: "ldap://localhost/dc=test",
+ host: "localhost",
+ asciiHost: "localhost",
+ port: -1,
+ scheme: "ldap",
+ path: "/dc=test",
+ prePath: "ldap://localhost",
+ hostPort: "localhost",
+ displaySpec: "ldap://localhost/dc=test",
+ displayPrePath: "ldap://localhost",
+ displayHost: "localhost",
+ displayHostPort: "localhost",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: 0,
+ },
+ {
+ url: "ldap://localhost:389/dc=test,dc=abc??sub?(objectclass=*)",
+ spec:
+ "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)",
+ asciiSpec:
+ "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)",
+ host: "localhost",
+ asciiHost: "localhost",
+ port: usingWallet ? 389 : -1,
+ scheme: "ldap",
+ path: "/dc=test,dc=abc??sub?(objectclass=*)",
+ prePath: "ldap://localhost" + portAdpt,
+ hostPort: "localhost" + portAdpt,
+ displaySpec:
+ "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)",
+ displayPrePath: "ldap://localhost",
+ displayHost: "localhost",
+ displayHostPort: "localhost" + portAdpt,
+ dn: "dc=test,dc=abc",
+ scope: Ci.nsILDAPURL.SCOPE_SUBTREE,
+ filter: "(objectclass=*)",
+ options: 0,
+ },
+ {
+ url: "ldap://\u65e5\u672c\u8a93.jp:389/dc=tes\u65e5t??one?(oc=xyz)",
+ spec:
+ "ldap://xn--wgv71a309e.jp" + portAdpt + "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ asciiSpec:
+ "ldap://xn--wgv71a309e.jp" + portAdpt + "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ host: "xn--wgv71a309e.jp",
+ asciiHost: "xn--wgv71a309e.jp",
+ port: usingWallet ? 389 : -1,
+ scheme: "ldap",
+ path: "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ prePath: "ldap://xn--wgv71a309e.jp" + portAdpt,
+ hostPort: "xn--wgv71a309e.jp" + portAdpt,
+ displaySpec:
+ "ldap://\u65e5\u672c\u8a93.jp" +
+ portAdpt +
+ "/dc=tes%E6%97%A5t??one?(oc=xyz)",
+ displayPrePath: "ldap://\u65e5\u672c\u8a93.jp" + portAdpt,
+ displayHost: "\u65e5\u672c\u8a93.jp",
+ displayHostPort: "\u65e5\u672c\u8a93.jp" + portAdpt,
+ dn: "dc=tes\u65e5t",
+ scope: Ci.nsILDAPURL.SCOPE_ONELEVEL,
+ filter: "(oc=xyz)",
+ options: 0,
+ },
+ {
+ url: "ldaps://localhost/dc=test",
+ spec: "ldaps://localhost/dc=test",
+ asciiSpec: "ldaps://localhost/dc=test",
+ host: "localhost",
+ asciiHost: "localhost",
+ port: -1,
+ scheme: "ldaps",
+ path: "/dc=test",
+ prePath: "ldaps://localhost",
+ hostPort: "localhost",
+ displaySpec: "ldaps://localhost/dc=test",
+ displayPrePath: "ldaps://localhost",
+ displayHost: "localhost",
+ displayHostPort: "localhost",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: Ci.nsILDAPURL.OPT_SECURE,
+ },
+ {
+ url: "ldaps://127.0.0.1/dc=test",
+ spec: "ldaps://127.0.0.1/dc=test",
+ asciiSpec: "ldaps://127.0.0.1/dc=test",
+ host: "127.0.0.1",
+ asciiHost: "127.0.0.1",
+ port: -1,
+ scheme: "ldaps",
+ path: "/dc=test",
+ prePath: "ldaps://127.0.0.1",
+ hostPort: "127.0.0.1",
+ displaySpec: "ldaps://127.0.0.1/dc=test",
+ displayPrePath: "ldaps://127.0.0.1",
+ displayHost: "127.0.0.1",
+ displayHostPort: "127.0.0.1",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: Ci.nsILDAPURL.OPT_SECURE,
+ },
+ {
+ url: "ldaps://[::1]/dc=test",
+ spec: "ldaps://[::1]/dc=test",
+ asciiSpec: "ldaps://[::1]/dc=test",
+ host: "::1",
+ asciiHost: "::1",
+ port: -1,
+ scheme: "ldaps",
+ path: "/dc=test",
+ prePath: "ldaps://[::1]",
+ hostPort: "[::1]",
+ displaySpec: "ldaps://[::1]/dc=test",
+ displayPrePath: "ldaps://[::1]",
+ displayHost: "::1",
+ displayHostPort: "[::1]",
+ dn: "dc=test",
+ scope: Ci.nsILDAPURL.SCOPE_BASE,
+ filter: "(objectclass=*)",
+ options: Ci.nsILDAPURL.OPT_SECURE,
+ },
+];
+
+function run_test() {
+ var url;
+
+ // Test - get and check urls.
+
+ for (let part = 0; part < ldapURLs.length; ++part) {
+ dump("url: " + ldapURLs[part].url + "\n");
+ url = Services.io.newURI(ldapURLs[part].url);
+
+ Assert.equal(url.spec, ldapURLs[part].spec);
+ Assert.equal(url.asciiSpec, ldapURLs[part].asciiSpec);
+ Assert.equal(url.scheme, ldapURLs[part].scheme);
+ Assert.equal(url.host, ldapURLs[part].host);
+ Assert.equal(url.asciiHost, ldapURLs[part].asciiHost);
+ Assert.equal(url.port, ldapURLs[part].port);
+ Assert.equal(url.pathQueryRef, ldapURLs[part].path);
+ Assert.equal(url.prePath, ldapURLs[part].prePath);
+ Assert.equal(url.hostPort, ldapURLs[part].hostPort);
+ Assert.equal(url.displaySpec, ldapURLs[part].displaySpec);
+ Assert.equal(url.displayPrePath, ldapURLs[part].displayPrePath);
+ Assert.equal(url.displayHost, ldapURLs[part].displayHost);
+ Assert.equal(url.displayHostPort, ldapURLs[part].displayHostPort);
+ // XXX nsLDAPURL ought to have classinfo.
+ url = url.QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.dn, ldapURLs[part].dn);
+ Assert.equal(url.scope, ldapURLs[part].scope);
+ Assert.equal(url.filter, ldapURLs[part].filter);
+ Assert.equal(url.options, ldapURLs[part].options);
+ }
+
+ // Test - Check changing ldap values
+ dump("Other Tests\n");
+
+ // Start off with a base url
+ const kBaseURL = "ldap://localhost:389/dc=test,dc=abc??sub?(objectclass=*)";
+
+ url = Services.io.newURI(kBaseURL).QueryInterface(Ci.nsILDAPURL);
+
+ // Test - dn
+
+ url.dn = "dc=short";
+
+ Assert.equal(url.dn, "dc=short");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??sub?(objectclass=*)"
+ );
+
+ // Test - scope
+
+ url.scope = Ci.nsILDAPURL.SCOPE_BASE;
+
+ Assert.equal(url.scope, Ci.nsILDAPURL.SCOPE_BASE);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short???(objectclass=*)"
+ );
+
+ url.scope = Ci.nsILDAPURL.SCOPE_ONELEVEL;
+
+ Assert.equal(url.scope, Ci.nsILDAPURL.SCOPE_ONELEVEL);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Test - filter
+
+ url.filter = "(&(oc=ygh)(l=Ереван))";
+
+ Assert.equal(url.filter, "(&(oc=ygh)(l=Ереван))");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" +
+ portAdpt +
+ "/dc=short??one?(&(oc=ygh)(l=%D0%95%D1%80%D0%B5%D0%B2%D0%B0%D0%BD))"
+ );
+
+ url.filter = "";
+
+ Assert.equal(url.filter, "(objectclass=*)");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Test - scheme
+
+ // An old version used to have a bug whereby if you set the scheme to the
+ // same thing twice, you'd get the options set wrongly.
+ url = url
+ .mutate()
+ .setScheme("ldaps")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 1);
+ Assert.equal(
+ url.spec,
+ "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+ url = url
+ .mutate()
+ .setScheme("ldaps")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 1);
+ Assert.equal(
+ url.spec,
+ "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ Assert.ok(url.schemeIs("ldaps"));
+ Assert.ok(!url.schemeIs("ldap"));
+
+ url = url.mutate().setScheme("ldap").finalize().QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+ url = url.mutate().setScheme("ldap").finalize().QueryInterface(Ci.nsILDAPURL);
+ Assert.equal(url.options, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ Assert.ok(url.schemeIs("ldap"));
+ Assert.ok(!url.schemeIs("ldaps"));
+
+ // Test - Options
+
+ url.options = Ci.nsILDAPURL.OPT_SECURE;
+
+ Assert.equal(url.options, Ci.nsILDAPURL.OPT_SECURE);
+ Assert.equal(
+ url.spec,
+ "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ url.options = 0;
+
+ Assert.equal(url.options, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Test - Equals
+
+ var url2 = Services.io
+ .newURI("ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)")
+ .QueryInterface(Ci.nsILDAPURL);
+
+ Assert.ok(url.equals(url2));
+
+ url2 = url2
+ .mutate()
+ .setSpec("ldap://localhost:389/dc=short??sub?(objectclass=*)")
+ .finalize();
+
+ Assert.ok(!url.equals(url2));
+
+ // Test Attributes
+
+ Assert.equal(url.attributes.length, 0);
+
+ // Nothing should happen if the attribute doesn't exist
+ url.removeAttribute("abc");
+
+ Assert.equal(url.attributes.length, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ url.addAttribute("dn");
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short?dn?one?(objectclass=*)"
+ );
+
+ Assert.equal(url.attributes, "dn");
+
+ url.removeAttribute("dn");
+
+ Assert.equal(url.attributes.length, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ var newAttrs = "abc,def,ghi,jkl";
+ url.attributes = newAttrs;
+
+ Assert.equal(url.attributes, newAttrs);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" +
+ portAdpt +
+ "/dc=short?" +
+ newAttrs +
+ "?one?(objectclass=*)"
+ );
+
+ // Try adding an existing attribute - should do nothing
+ url.addAttribute("def");
+ Assert.equal(url.attributes, newAttrs);
+
+ // url.addAttribute("jk");
+
+ Assert.ok(url.hasAttribute("jkl"));
+ Assert.ok(url.hasAttribute("def"));
+ Assert.ok(url.hasAttribute("ABC"));
+ Assert.ok(!url.hasAttribute("cde"));
+ Assert.ok(!url.hasAttribute("3446"));
+ Assert.ok(!url.hasAttribute("kl"));
+ Assert.ok(!url.hasAttribute("jk"));
+
+ // Sub-string of an attribute, so this shouldn't change anything.
+ url.removeAttribute("kl");
+ url.removeAttribute("jk");
+ url.removeAttribute("ef");
+ Assert.equal(url.attributes, newAttrs);
+
+ url.removeAttribute("abc");
+ newAttrs = newAttrs.substring(4);
+
+ Assert.equal(url.attributes, newAttrs);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" +
+ portAdpt +
+ "/dc=short?" +
+ newAttrs +
+ "?one?(objectclass=*)"
+ );
+
+ // This shouldn't fail, just clear the list
+ url.attributes = "";
+
+ Assert.equal(url.attributes.length, 0);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)"
+ );
+
+ // Set attributes via the url spec
+
+ newAttrs = "abc,def,ghi,jkl";
+ url = url
+ .mutate()
+ .setSpec("ldap://localhost/dc=short?" + newAttrs + "?one?(objectclass=*)")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+
+ Assert.equal(url.attributes, newAttrs);
+ Assert.equal(
+ url.spec,
+ "ldap://localhost/dc=short?" + newAttrs + "?one?(objectclass=*)"
+ );
+
+ url = url
+ .mutate()
+ .setSpec("ldap://localhost/dc=short??one?(objectclass=*)")
+ .finalize()
+ .QueryInterface(Ci.nsILDAPURL);
+
+ var attrs = url.attributes;
+ Assert.equal(attrs.length, 0);
+ Assert.equal(url.spec, "ldap://localhost/dc=short??one?(objectclass=*)");
+
+ // Test - clone
+
+ url = url
+ .mutate()
+ .setSpec("ldap://localhost/dc=short?abc,def,ghi,jkl?one?(objectclass=*)")
+ .finalize();
+
+ var newUrl = url.mutate().finalize();
+
+ Assert.equal(
+ newUrl.spec,
+ "ldap://localhost/dc=short?abc,def,ghi,jkl?one?(objectclass=*)"
+ );
+}
diff --git a/comm/mailnews/addrbook/test/unit/test_photoURL.js b/comm/mailnews/addrbook/test/unit/test_photoURL.js
new file mode 100644
index 0000000000..a6e7796264
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_photoURL.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+
+/**
+ * Tests that vCard photo data is correctly translated into a URL for display.
+ */
+add_task(async function testVCardPhotoURL() {
+ let jpegPrefix = "data:image/jpeg;base64,/9j/4AAQSkZJRgABA";
+ let pngPrefix = "data:image/png;base64,iVBORw0KGgoAAAANSU";
+
+ for (let [fileName, expectedURL] of [
+ // Version 3, binary data, binary value type
+ ["v3-binary-jpeg.vcf", jpegPrefix],
+ ["v3-binary-png.vcf", pngPrefix],
+ // Version 3, URI data, binary value type (mismatch)
+ ["v3-uri-binary-jpeg.vcf", jpegPrefix],
+ ["v3-uri-binary-png.vcf", pngPrefix],
+ // Version 3, URI data, URI value type
+ ["v3-uri-uri-jpeg.vcf", jpegPrefix],
+ ["v3-uri-uri-png.vcf", pngPrefix],
+ // Version 4, URI data, URI value type
+ ["v4-uri-jpeg.vcf", jpegPrefix],
+ ["v4-uri-png.vcf", pngPrefix],
+ ]) {
+ info(`testing ${fileName}`);
+ let file = do_get_file(`data/${fileName}`);
+ let vCard = await IOUtils.readUTF8(file.path);
+ let card = VCardUtils.vCardToAbCard(vCard);
+
+ Assert.equal(card.photoURL.substring(0, 40), expectedURL);
+ }
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js b/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js
new file mode 100644
index 0000000000..e879b05cbc
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js
@@ -0,0 +1,79 @@
+/* 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/. */
+
+const { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+
+/**
+ * Tests that the mail.displayname.version preference is correctly incremented
+ * if a card's DisplayName or PreferDisplayName properties change.
+ */
+add_task(async function () {
+ function getPrefValue() {
+ return Services.prefs.getIntPref("mail.displayname.version", -999);
+ }
+
+ /**
+ * Effectively the same as the function of the same name in nsMsgDBView.cpp.
+ * This proves the cardForEmailAddress cache in AddrBookManager is correctly
+ * cleared when the preference changes.
+ */
+ function getDisplayNameInAddressBook() {
+ let card = MailServices.ab.cardForEmailAddress("first.last@invalid");
+ if (!card) {
+ return null;
+ }
+
+ let preferDisplayName = card.getPropertyAsBool("PreferDisplayName", true);
+ return preferDisplayName ? card.displayName : card.primaryEmail;
+ }
+
+ Assert.equal(getPrefValue(), -999, "pref has no initial value");
+ Assert.equal(getDisplayNameInAddressBook(), null, "card doesn't exist yet");
+
+ let book = MailServices.ab.getDirectory(kPABData.URI);
+ let card = new AddrBookCard();
+ card.firstName = "first";
+ card.lastName = "last";
+ card.displayName = "first last";
+ card.primaryEmail = "first.last@invalid";
+ book.addCard(card);
+
+ Assert.equal(getPrefValue(), 1, "pref created by adding card");
+ Assert.equal(getDisplayNameInAddressBook(), "first last");
+
+ [card] = book.childCards;
+ card.displayName = "display";
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 2, "pref updated by changing display name");
+ Assert.equal(getDisplayNameInAddressBook(), "display");
+
+ [card] = book.childCards;
+ card.setPropertyAsBool("PreferDisplayName", true);
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 3, "pref updated by adding flag");
+ Assert.equal(getDisplayNameInAddressBook(), "display");
+
+ [card] = book.childCards;
+ card.displayName = "display name";
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 4, "pref updated by changing display name");
+ Assert.equal(getDisplayNameInAddressBook(), "display name");
+
+ [card] = book.childCards;
+ card.setPropertyAsBool("PreferDisplayName", false);
+ book.modifyCard(card);
+
+ Assert.equal(getPrefValue(), 5, "pref updated by clearing flag");
+ Assert.equal(getDisplayNameInAddressBook(), "first.last@invalid");
+
+ book.deleteCards([card]);
+
+ Assert.equal(getPrefValue(), 6, "pref updated by deleting card");
+ Assert.equal(getDisplayNameInAddressBook(), null, "card no longer exists");
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_search.js b/comm/mailnews/addrbook/test/unit/test_search.js
new file mode 100644
index 0000000000..c25ba17b96
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_search.js
@@ -0,0 +1,65 @@
+/* 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";
+
+const { getModelQuery, generateQueryURI } = ChromeUtils.import(
+ "resource:///modules/ABQueryUtils.jsm"
+);
+
+const jsonFile = do_get_file("data/ldap_contacts.json");
+
+add_task(async () => {
+ let contacts = await IOUtils.readJSON(jsonFile.path);
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "new book",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ for (let [name, { attributes }] of Object.entries(contacts)) {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ card.displayName = attributes.cn;
+ card.firstName = attributes.givenName;
+ card.lastName = attributes.sn;
+ card.primaryEmail = attributes.mail;
+ contacts[name] = book.addCard(card);
+ }
+
+ let doSearch = async function (searchString, ...expectedContacts) {
+ let foundCards = await new Promise(resolve => {
+ let listener = {
+ cards: [],
+ onSearchFoundCard(card) {
+ this.cards.push(card);
+ },
+ onSearchFinished(status, complete, secInfo, location) {
+ resolve(this.cards);
+ },
+ };
+ book.search(searchString, "", listener);
+ });
+
+ Assert.equal(foundCards.length, expectedContacts.length);
+ for (let name of expectedContacts) {
+ Assert.ok(foundCards.find(c => c.equals(contacts[name])));
+ }
+ };
+
+ await doSearch("(DisplayName,c,watson)", "john", "mary");
+
+ let modelQuery = getModelQuery("mail.addr_book.autocompletequery.format");
+ await doSearch(
+ generateQueryURI(modelQuery, ["holmes"]),
+ "eurus",
+ "mycroft",
+ "sherlock"
+ );
+ await doSearch(generateQueryURI(modelQuery, ["adler"]), "irene");
+ await doSearch(generateQueryURI(modelQuery, ["redbeard"]));
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_vCard.js b/comm/mailnews/addrbook/test/unit/test_vCard.js
new file mode 100644
index 0000000000..328be1c8cd
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_vCard.js
@@ -0,0 +1,474 @@
+/* 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/. */
+
+let { VCardProperties, VCardUtils } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+const ANY_UID = "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+
+add_task(function testVCardToPropertyMap() {
+ function check(vCardLine, expectedProps) {
+ let vCard = `BEGIN:VCARD\r\n${vCardLine}\r\nEND:VCARD\r\n`;
+ info(vCard);
+ let properties = VCardProperties.fromVCard(vCard).toPropertyMap();
+ // Check that every property in expectedProps is present in `properties`.
+ // No other property can be present unless it is in `propWhitelist`.
+ for (let [name, value] of properties) {
+ if (name in expectedProps) {
+ Assert.equal(value, expectedProps[name], `expected ${name}`);
+ delete expectedProps[name];
+ } else {
+ Assert.ok(false, `card should not have property '${name}'`);
+ }
+ }
+
+ for (let name of Object.keys(expectedProps)) {
+ Assert.ok(false, `expected ${name} not found`);
+ }
+ }
+
+ // Name
+ check("N:Last;First", { FirstName: "First", LastName: "Last" });
+ check("N:Last;First;;;", { FirstName: "First", LastName: "Last" });
+ check("N:Last;First;Middle;Prefix;Suffix", {
+ FirstName: "First",
+ LastName: "Last",
+ AdditionalNames: "Middle",
+ NamePrefix: "Prefix",
+ NameSuffix: "Suffix",
+ });
+ check("N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.", {
+ FirstName: "John",
+ LastName: "Stevenson",
+ AdditionalNames: "Philip Paul",
+ NamePrefix: "Dr.",
+ NameSuffix: "Jr. M.D. A.C.P.",
+ });
+
+ // Address
+ check(
+ "ADR:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ {
+ WorkPOBox: "PO Box 3.14",
+ WorkAddress2: "Apartment 4",
+ WorkAddress: "123 Main Street",
+ WorkCity: "Any Town",
+ WorkState: "CA",
+ WorkZipCode: "91921-1234",
+ WorkCountry: "U.S.A.",
+ }
+ );
+ check(
+ "ADR;TYPE=work:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ {
+ WorkPOBox: "PO Box 3.14",
+ WorkAddress2: "Apartment 4",
+ WorkAddress: "123 Main Street",
+ WorkCity: "Any Town",
+ WorkState: "CA",
+ WorkZipCode: "91921-1234",
+ WorkCountry: "U.S.A.",
+ }
+ );
+ check(
+ "ADR;TYPE=home:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ {
+ HomePOBox: "PO Box 3.14",
+ HomeAddress2: "Apartment 4",
+ HomeAddress: "123 Main Street",
+ HomeCity: "Any Town",
+ HomeState: "CA",
+ HomeZipCode: "91921-1234",
+ HomeCountry: "U.S.A.",
+ }
+ );
+
+ // Phone
+ check("TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=work:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=home:11-2358-13-21", { HomePhone: "11-2358-13-21" });
+ check("TEL;TYPE=cell:11-2358-13-21", { CellularNumber: "11-2358-13-21" });
+ check("TEL;TYPE=pager:11-2358-13-21", { PagerNumber: "11-2358-13-21" });
+ check("TEL;TYPE=fax:11-2358-13-21", { FaxNumber: "11-2358-13-21" });
+
+ check("TEL;TYPE=work;PREF:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=work,cell:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+ check("TEL;TYPE=work;TYPE=cell:11-2358-13-21", {
+ WorkPhone: "11-2358-13-21",
+ });
+ check("TEL;TYPE=work;VALUE=TEXT:11-2358-13-21", {
+ WorkPhone: "11-2358-13-21",
+ });
+ check("TEL;TYPE=home;VALUE=TEXT:011-2358-13-21", {
+ HomePhone: "011-2358-13-21",
+ });
+ check(
+ "TEL;TYPE=work;VALUE=TEXT:11-2358-13-21\r\nTEL;TYPE=home;VALUE=TEXT:011-2358-13-21",
+ {
+ WorkPhone: "11-2358-13-21",
+ HomePhone: "011-2358-13-21",
+ }
+ );
+ check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", {
+ CellularNumber: "11-2358-13-21",
+ });
+ check("TEL;TYPE=cell;PREF=1:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", {
+ CellularNumber: "11-2358-13-21",
+ });
+ check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell;PREF=1:011-2358-13-21", {
+ CellularNumber: "011-2358-13-21",
+ });
+
+ // Birthday
+ check("BDAY;VALUE=DATE:19830403", {
+ BirthDay: "3",
+ BirthMonth: "4",
+ BirthYear: "1983",
+ });
+ check("BDAY:--0415", { BirthDay: "15", BirthMonth: "4" });
+ check("BDAY:2001", { BirthYear: "2001" });
+ check("BDAY:2006-06", { BirthYear: "2006", BirthMonth: "6" });
+ check("BDAY:--12", { BirthMonth: "12" });
+ check("BDAY:---30", { BirthDay: "30" });
+ // These are error cases, testing that it doesn't throw.
+ check("BDAY;VALUE=DATE:NaN-NaN-NaN", {});
+ check("BDAY;VALUE=TEXT:07/07/1949", {});
+
+ // Anniversary
+ check("ANNIVERSARY;VALUE=DATE:20041207", {
+ AnniversaryDay: "7",
+ AnniversaryMonth: "12",
+ AnniversaryYear: "2004",
+ });
+
+ // Organization: any number of values is valid here.
+ check("ORG:Acme Widgets, Inc.", {
+ Company: "Acme Widgets, Inc.",
+ });
+ check("ORG:Acme Widgets, Inc.;Manufacturing", {
+ Company: "Acme Widgets, Inc.",
+ Department: "Manufacturing",
+ });
+ check("ORG:Acme Widgets, Inc.;Manufacturing;Thingamies", {
+ Company: "Acme Widgets, Inc.",
+ Department: "Manufacturing",
+ });
+
+ // URL
+ // If no type is given assume its WebPage1 (work).
+ check("URL:https://www.thunderbird.net/", {
+ WebPage1: "https://www.thunderbird.net/",
+ });
+
+ check("URL;TYPE=work:https://developer.thunderbird.net/", {
+ WebPage1: "https://developer.thunderbird.net/",
+ });
+
+ check("URL;TYPE=home:https://addons.thunderbird.net/", {
+ WebPage2: "https://addons.thunderbird.net/",
+ });
+
+ check(
+ formatVCard`
+ URL;TYPE=home:https://addons.thunderbird.net/
+ URL;TYPE=work:https://developer.thunderbird.net/`,
+ {
+ WebPage1: "https://developer.thunderbird.net/",
+ WebPage2: "https://addons.thunderbird.net/",
+ }
+ );
+
+ // If a URL without a type is given and a Work Web Page do not import the URL without type.
+ check(
+ formatVCard`
+ URL:https://www.thunderbird.net/
+ URL;TYPE=home:https://addons.thunderbird.net/
+ URL;TYPE=work:https://developer.thunderbird.net/`,
+ {
+ WebPage1: "https://developer.thunderbird.net/",
+ WebPage2: "https://addons.thunderbird.net/",
+ }
+ );
+ // Email: just to be difficult, email is stored by priority, not type.
+ check("EMAIL:first@invalid", { PrimaryEmail: "first@invalid" });
+ check("EMAIL;PREF=1:first@invalid", { PrimaryEmail: "first@invalid" });
+
+ check("EMAIL;PREF=1:first@invalid\r\nEMAIL:second@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+ check("EMAIL:second@invalid\r\nEMAIL;PREF=1:first@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+
+ check("EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+ check("EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid", {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ });
+
+ check(
+ "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+ check(
+ "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid",
+ {
+ PrimaryEmail: "first@invalid",
+ SecondEmail: "second@invalid",
+ }
+ );
+
+ // Group-prefixed properties.
+ check(
+ formatVCard`
+ item1.EMAIL:first@invalid
+ item1.X-ABLabel:First`,
+ {
+ PrimaryEmail: "first@invalid",
+ }
+ );
+ check(
+ formatVCard`
+ item1.EMAIL:first@invalid
+ item1.X-ABLabel:First
+ item2.EMAIL:second@invalid
+ item2.X-ABLabel:Second`,
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" }
+ );
+ check(
+ formatVCard`
+ foo-bar.EMAIL:first@invalid
+ foo-bar.X-ABLabel:First
+ EMAIL:second@invalid`,
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" }
+ );
+ check(
+ formatVCard`
+ EMAIL:first@invalid
+ abc.EMAIL:second@invalid
+ abc.X-ABLabel:Second`,
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" }
+ );
+ check("xyz.TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" });
+});
+
+add_task(function testAbCardToVCard() {
+ function check(abCardProps, ...expectedLines) {
+ let abCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (let [name, value] of Object.entries(abCardProps)) {
+ if (name == "UID") {
+ abCard.UID = abCardProps.UID;
+ continue;
+ }
+ abCard.setProperty(name, value);
+ }
+
+ let vCard = VCardUtils.abCardToVCard(abCard);
+ info(vCard);
+ let vCardLines = vCard.split("\r\n");
+ if (expectedLines.includes(ANY_UID)) {
+ for (let i = 0; i < vCardLines.length; i++) {
+ if (vCardLines[i].startsWith("UID:")) {
+ vCardLines[i] = ANY_UID;
+ }
+ }
+ }
+
+ for (let line of expectedLines) {
+ Assert.ok(vCardLines.includes(line), line);
+ }
+ }
+
+ // UID
+ check(
+ {
+ UID: "12345678-1234-1234-1234-123456789012",
+ },
+ "UID:12345678-1234-1234-1234-123456789012"
+ );
+
+ // Name
+ check(
+ {
+ FirstName: "First",
+ LastName: "Last",
+ },
+ "N:Last;First;;;",
+ ANY_UID
+ );
+ check(
+ {
+ FirstName: "First",
+ LastName: "Last",
+ AdditionalNames: "Middle",
+ NamePrefix: "Prefix",
+ NameSuffix: "Suffix",
+ },
+ "N:Last;First;Middle;Prefix;Suffix",
+ ANY_UID
+ );
+ check(
+ {
+ FirstName: "First",
+ LastName: "Last",
+ NameSuffix: "Suffix",
+ },
+ "N:Last;First;;;Suffix",
+ ANY_UID
+ );
+
+ // Address
+ check(
+ {
+ WorkAddress: "123 Main Street",
+ WorkCity: "Any Town",
+ WorkState: "CA",
+ WorkZipCode: "91921-1234",
+ WorkCountry: "U.S.A.",
+ },
+ "ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ ANY_UID
+ );
+ check(
+ {
+ HomeAddress: "123 Main Street",
+ HomeCity: "Any Town",
+ HomeState: "CA",
+ HomeZipCode: "91921-1234",
+ HomeCountry: "U.S.A.",
+ },
+ "ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.",
+ ANY_UID
+ );
+
+ // Phone
+ check(
+ {
+ WorkPhone: "11-2358-13-21",
+ },
+ "TEL;VALUE=TEXT:11-2358-13-21",
+ ANY_UID
+ );
+ check(
+ {
+ HomePhone: "011-2358-13-21",
+ },
+ "TEL;VALUE=TEXT:011-2358-13-21",
+ ANY_UID
+ );
+ check(
+ {
+ WorkPhone: "11-2358-13-21",
+ HomePhone: "011-2358-13-21",
+ },
+ "TEL;TYPE=work;VALUE=TEXT:11-2358-13-21",
+ "TEL;TYPE=home;VALUE=TEXT:011-2358-13-21",
+ ANY_UID
+ );
+
+ // Birthday
+ check(
+ {
+ BirthDay: "3",
+ BirthMonth: "4",
+ BirthYear: "1983",
+ },
+ "BDAY;VALUE=DATE:19830403",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "3",
+ BirthMonth: "4",
+ BirthYear: "", // No value.
+ },
+ "BDAY;VALUE=DATE:--0403",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "3",
+ BirthMonth: "4",
+ // BirthYear missing altogether.
+ },
+ "BDAY;VALUE=DATE:--0403",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "", // No value.
+ BirthMonth: "", // No value.
+ BirthYear: "1983",
+ },
+ "BDAY;VALUE=DATE:1983",
+ ANY_UID
+ );
+ check(
+ {
+ BirthDay: "", // No value.
+ BirthMonth: "", // No value.
+ BirthYear: "", // No value.
+ },
+ ANY_UID
+ );
+
+ // Anniversary
+ check(
+ {
+ AnniversaryDay: "7",
+ AnniversaryMonth: "12",
+ AnniversaryYear: "2004",
+ },
+ "ANNIVERSARY;VALUE=DATE:20041207",
+ ANY_UID
+ );
+
+ // Email
+ check({ PrimaryEmail: "first@invalid" }, "EMAIL;PREF=1:first@invalid");
+ check({ SecondEmail: "second@invalid" }, "EMAIL:second@invalid");
+ check(
+ { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" },
+ "EMAIL;PREF=1:first@invalid",
+ "EMAIL:second@invalid"
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_vCard21.js b/comm/mailnews/addrbook/test/unit/test_vCard21.js
new file mode 100644
index 0000000000..28fb1b21d4
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_vCard21.js
@@ -0,0 +1,190 @@
+/* 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/. */
+
+let { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm");
+
+add_task(async () => {
+ function check(vCardLines, expectedProps) {
+ checkWithCase(vCardLines, expectedProps.slice(), false);
+ checkWithCase(
+ vCardLines,
+ expectedProps.map(p => {
+ if (p.params?.type) {
+ p.params.type = p.params.type.toLowerCase();
+ }
+ return p;
+ }),
+ true
+ );
+ }
+
+ function checkWithCase(vCardLines, expectedProps, lowerCase) {
+ let vCard = `BEGIN:VCARD\r\nVERSION:2.1\r\n${vCardLines}\r\nEND:VCARD\r\n`;
+ if (lowerCase) {
+ vCard = vCard.toLowerCase();
+ }
+ info(vCard);
+ let abCard = VCardUtils.vCardToAbCard(vCard);
+ for (let propertyEntry of abCard.vCardProperties.entries) {
+ let index = expectedProps.findIndex(
+ p =>
+ p.name == propertyEntry.name &&
+ p.value.toString() == propertyEntry.value.toString()
+ );
+ Assert.greater(index, -1);
+ let [prop] = expectedProps.splice(index, 1);
+ Assert.deepEqual(propertyEntry.params, prop.params ?? {});
+ }
+
+ for (let { name, value } of expectedProps) {
+ ok(false, `expected ${name}=${value} not found`);
+ }
+ }
+
+ // Different types of phone number.
+ check("TEL:1234567", [{ name: "tel", value: "1234567" }]);
+ check("TEL;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { pref: 1 } },
+ ]);
+ check("TEL;CELL:1234567", [
+ { name: "tel", value: "1234567", params: { type: "CELL" } },
+ ]);
+ check("TEL;CELL;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } },
+ ]);
+ check("TEL;HOME:1234567", [
+ { name: "tel", value: "1234567", params: { type: "HOME" } },
+ ]);
+ check("TEL;HOME;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { type: "HOME", pref: 1 } },
+ ]);
+ check("TEL;VOICE:1234567", [{ name: "tel", value: "1234567" }]);
+ check("TEL;VOICE;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { pref: 1 } },
+ ]);
+ check("TEL;WORK:1234567", [
+ { name: "tel", value: "1234567", params: { type: "WORK" } },
+ ]);
+ check("TEL;WORK;PREF:1234567", [
+ { name: "tel", value: "1234567", params: { type: "WORK", pref: 1 } },
+ ]);
+
+ // Combinations of phone number types.
+ check("TEL;CELL:1234567\r\nTEL;HOME:9876543", [
+ { name: "tel", value: "1234567", params: { type: "CELL" } },
+ { name: "tel", value: "9876543", params: { type: "HOME" } },
+ ]);
+ check("TEL;CELL;PREF:1234567\r\nTEL;HOME:9876543", [
+ { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } },
+ { name: "tel", value: "9876543", params: { type: "HOME" } },
+ ]);
+
+ // Phone number preference.
+ check("TEL;CELL;PREF:1234567\r\nTEL;CELL:9876543", [
+ { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } },
+ { name: "tel", value: "9876543", params: { type: "CELL" } },
+ ]);
+ check("TEL;CELL:1234567\r\nTEL;CELL;PREF:9876543", [
+ { name: "tel", value: "9876543", params: { type: "CELL", pref: 1 } },
+ { name: "tel", value: "1234567", params: { type: "CELL" } },
+ ]);
+
+ // Different types of email.
+ check("EMAIL:pref@invalid", [{ name: "email", value: "pref@invalid" }]);
+ check("EMAIL;PREF:pref@invalid", [
+ { name: "email", value: "pref@invalid", params: { pref: 1 } },
+ ]);
+ check("EMAIL;WORK:work@invalid", [
+ { name: "email", value: "work@invalid", params: { type: "WORK" } },
+ ]);
+ check("EMAIL;WORK;PREF:work@invalid", [
+ { name: "email", value: "work@invalid", params: { type: "WORK", pref: 1 } },
+ ]);
+ check("EMAIL;HOME:home@invalid", [
+ { name: "email", value: "home@invalid", params: { type: "HOME" } },
+ ]);
+ check("EMAIL;HOME;PREF:home@invalid", [
+ { name: "email", value: "home@invalid", params: { type: "HOME", pref: 1 } },
+ ]);
+ check("EMAIL;INTERNET:mail@invalid", [
+ { name: "email", value: "mail@invalid" },
+ ]);
+
+ // Email preference.
+ check("EMAIL;PREF:pref@invalid\r\nEMAIL:other@invalid", [
+ { name: "email", value: "pref@invalid", params: { pref: 1 } },
+ { name: "email", value: "other@invalid" },
+ ]);
+ check("EMAIL:other@invalid\r\nEMAIL;PREF:pref@invalid", [
+ { name: "email", value: "pref@invalid", params: { pref: 1 } },
+ { name: "email", value: "other@invalid" },
+ ]);
+
+ // Address types. Multiple types are allowed, some we don't care about.
+ check("ADR:;;street;town;state", [
+ { name: "adr", value: ["", "", "street", "town", "state"] },
+ ]);
+ check("ADR;WORK:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "WORK" },
+ },
+ ]);
+ check("ADR;HOME:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "HOME" },
+ },
+ ]);
+ check("ADR;DOM:;;street;town;state", [
+ { name: "adr", value: ["", "", "street", "town", "state"] },
+ ]);
+ check("ADR;POSTAL;WORK:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "WORK" },
+ },
+ ]);
+ check("ADR;PARCEL;HOME:;;street;town;state", [
+ {
+ name: "adr",
+ value: ["", "", "street", "town", "state"],
+ params: { type: "HOME" },
+ },
+ ]);
+
+ // Quoted-printable handling.
+ check("FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=74=C3=A9=24=74=20=23=31", [
+ { name: "fn", value: "té$t #1" },
+ ]);
+ check(
+ "FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=74=65=73=74=20=F0=9F=92=A9",
+ [{ name: "fn", value: "test 💩" }]
+ );
+ check("ORG;QUOTED-PRINTABLE:=74=65=73=74 #3", [
+ { name: "org", value: "test #3" },
+ ]);
+ check("N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=C5=82ast;=C6=92irst", [
+ { name: "n", value: ["łast", "ƒirst"] },
+ ]);
+ check(
+ "NOTE;QUOTED-PRINTABLE:line 1=0D=0A=\nline 2=0D=0A=\nline 3\r\nNICKNAME:foo=\r\nTITLE:bar=",
+ [
+ { name: "note", value: "line 1\r\nline 2\r\nline 3" },
+ { name: "nickname", value: "foo=" },
+ { name: "title", value: "bar=" },
+ ]
+ );
+ check(
+ "NOTE;QUOTED-PRINTABLE:line 1=0D=0A=\r\nline 2=0D=0A=\r\nline 3\r\nNICKNAME:foo=\r\nTITLE:bar=",
+ [
+ { name: "note", value: "line 1\r\nline 2\r\nline 3" },
+ { name: "nickname", value: "foo=" },
+ { name: "title", value: "bar=" },
+ ]
+ );
+});
diff --git a/comm/mailnews/addrbook/test/unit/test_vCardProperties.js b/comm/mailnews/addrbook/test/unit/test_vCardProperties.js
new file mode 100644
index 0000000000..cf2c28a634
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/test_vCardProperties.js
@@ -0,0 +1,899 @@
+/* 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/. */
+
+/* Tests of VCardProperties and VCardPropertyEntry. */
+
+var { AddrBookCard } = ChromeUtils.import(
+ "resource:///modules/AddrBookCard.jsm"
+);
+var { VCardProperties, VCardPropertyEntry } = ChromeUtils.import(
+ "resource:///modules/VCardUtils.jsm"
+);
+
+function propertyEqual(actual, expected, message) {
+ let actualAsObject = {
+ name: actual.name,
+ params: actual.params,
+ type: actual.type,
+ value: actual.value,
+ };
+ Assert.deepEqual(actualAsObject, expected, message);
+}
+
+function propertyArrayEqual(actual, expected, message) {
+ Assert.deepEqual(
+ actual.map(a => {
+ return {
+ name: a.name,
+ params: a.params,
+ type: a.type,
+ value: a.value,
+ };
+ }),
+ expected,
+ message
+ );
+}
+
+/**
+ * Tests that AddrBookCard supports vCard.
+ */
+add_task(function testAddrBookCard() {
+ let card = new AddrBookCard();
+ Assert.equal(card.supportsVCard, true, "AddrBookCard supports vCard");
+ Assert.ok(card.vCardProperties, "AddrBookCard has vCardProperties");
+ Assert.equal(card.vCardProperties.constructor.name, "VCardProperties");
+});
+
+/**
+ * Tests that nsAbCardProperty does not support vCard.
+ */
+add_task(function testABCardProperty() {
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ Assert.equal(
+ card.supportsVCard,
+ false,
+ "nsAbCardProperty does not support vCard"
+ );
+ Assert.strictEqual(
+ card.vCardProperties,
+ null,
+ "nsAbCardProperty has no vCardProperties"
+ );
+});
+
+/**
+ * Tests the `clone` and `equals` functions of VCardPropertyEntry, with a
+ * simple value type.
+ */
+add_task(function testPropertyEntrySingleValue() {
+ let entry = new VCardPropertyEntry("fn", {}, "text", "Juliet");
+ let clone = entry.clone();
+
+ Assert.ok(entry.equals(entry), "original is equal to itself");
+ Assert.ok(entry.equals(clone), "original is equal to cloned object");
+ Assert.ok(clone.equals(entry), "cloned object is equal to original");
+ Assert.ok(clone.equals(clone), "cloned object is equal to itself");
+
+ Assert.equal(clone.value, entry.value, "values are identical");
+
+ let other = new VCardPropertyEntry("n", {}, "text", "Romeo");
+ Assert.ok(!entry.equals(other), "original is not equal to another object");
+ Assert.ok(!other.equals(entry), "another object is not equal to original");
+});
+
+/**
+ * Tests the `clone` and `equals` functions of VCardPropertyEntry, with a
+ * complex value type.
+ */
+add_task(function testPropertyEntryMultiValue() {
+ // A name entry for somebody named "Mr One Two Three Four Senior".
+ let entry = new VCardPropertyEntry("n", {}, "text", [
+ "Four",
+ "One",
+ ["Two", "Three"],
+ "Mr",
+ "Senior",
+ ]);
+ let clone = entry.clone();
+
+ Assert.ok(entry.equals(entry), "original is equal to itself");
+ Assert.ok(entry.equals(clone), "original is equal to cloned object");
+ Assert.ok(clone.equals(entry), "cloned object is equal to original");
+ Assert.ok(clone.equals(clone), "cloned object is equal to itself");
+
+ Assert.deepEqual(clone.value, entry.value, "values are identical");
+
+ Assert.notEqual(
+ clone.value,
+ entry.value,
+ "value arrays are separate objects"
+ );
+ Assert.notEqual(
+ clone.value[2],
+ entry.value[2],
+ "subvalue arrays are separate objects"
+ );
+
+ // A name entry for somebody named "Mr One Two Three Four Junior".
+ let other = new VCardPropertyEntry("n", {}, "text", [
+ "Four",
+ "One",
+ ["Two", "Three"],
+ "Mr",
+ "Junior",
+ ]);
+ Assert.ok(!entry.equals(other), "original is not equal to another object");
+ Assert.ok(!other.equals(entry), "another object is not equal to original");
+});
+
+/**
+ * Tests creating a VCardProperties from a vCard string,
+ * then recreating the vCard.
+ */
+add_task(function testFromToVCard() {
+ let inVCard = formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ FN:Mike Test
+ N:Test;Mike;;;
+ EMAIL;PREF=1:mike@test.invalid
+ NICKNAME:Testing Mike
+ CATEGORIES:testers,quality control,QA
+ END:VCARD`;
+ let properties = VCardProperties.fromVCard(inVCard);
+
+ Assert.equal(properties.entries.length, 6, "entry count");
+ propertyEqual(
+ properties.getFirstEntry("version"),
+ {
+ name: "version",
+ params: {},
+ type: "text",
+ value: "3.0",
+ },
+ "version entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("fn"),
+ {
+ name: "fn",
+ params: {},
+ type: "text",
+ value: "Mike Test",
+ },
+ "fn entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("n"),
+ {
+ name: "n",
+ params: {},
+ type: "text",
+ value: ["Test", "Mike", "", "", ""],
+ },
+ "n entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("email"),
+ {
+ name: "email",
+ params: { pref: 1 },
+ type: "text",
+ value: "mike@test.invalid",
+ },
+ "email entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("nickname"),
+ {
+ name: "nickname",
+ params: {},
+ type: "text",
+ value: "Testing Mike",
+ },
+ "multivalue entry with one value"
+ );
+ propertyEqual(
+ properties.getFirstEntry("categories"),
+ {
+ name: "categories",
+ params: {},
+ type: "text",
+ value: ["testers", "quality control", "QA"],
+ },
+ "multivalue entry with multiple values"
+ );
+
+ let outVCard = properties.toVCard();
+ Assert.equal(outVCard, inVCard, "vCard reproduction");
+});
+
+/**
+ * Tests creating a VCardProperties from a Map of old-style address book
+ * properties, then recreating the Map.
+ */
+add_task(function testFromToPropertyMap() {
+ let inProperties = [
+ ["DisplayName", "Mike Test"],
+ ["LastName", "Test"],
+ ["FirstName", "Mike"],
+ ["PrimaryEmail", "mike@test.invalid"],
+ ["Custom1", "custom one"],
+ ["Custom2", "custom two"],
+ ["Custom3", "custom three"],
+ ["Custom4", "custom four"],
+ ];
+ let properties = VCardProperties.fromPropertyMap(
+ new Map(inProperties),
+ "3.0"
+ );
+
+ Assert.equal(properties.entries.length, 8, "entry count");
+ propertyEqual(
+ properties.getFirstEntry("version"),
+ {
+ name: "version",
+ params: {},
+ type: "text",
+ value: "3.0",
+ },
+ "version entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("fn"),
+ {
+ name: "fn",
+ params: {},
+ type: "text",
+ value: "Mike Test",
+ },
+ "fn entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("n"),
+ {
+ name: "n",
+ params: {},
+ type: "text",
+ value: ["Test", "Mike", "", "", ""],
+ },
+ "n entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("email"),
+ {
+ name: "email",
+ params: { pref: 1 },
+ type: "text",
+ value: "mike@test.invalid",
+ },
+ "email entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom1"),
+ {
+ name: "x-custom1",
+ params: {},
+ type: "text",
+ value: "custom one",
+ },
+ "custom1 entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom2"),
+ {
+ name: "x-custom2",
+ params: {},
+ type: "text",
+ value: "custom two",
+ },
+ "custom2 entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom3"),
+ {
+ name: "x-custom3",
+ params: {},
+ type: "text",
+ value: "custom three",
+ },
+ "custom3 entry"
+ );
+ propertyEqual(
+ properties.getFirstEntry("x-custom4"),
+ {
+ name: "x-custom4",
+ params: {},
+ type: "text",
+ value: "custom four",
+ },
+ "custom4 entry"
+ );
+
+ let outProperties = properties.toPropertyMap();
+ Assert.equal(outProperties.size, 8, "property count");
+ for (let [key, value] of inProperties) {
+ Assert.equal(outProperties.get(key), value, `${key} property`);
+ }
+
+ // Tests that `toPropertyMap` doesn't break multi-value entries, which could
+ // happen if `toAbCard` operates on the original entry instead of a clone.
+ properties = new VCardProperties();
+ properties.addEntry(
+ new VCardPropertyEntry("org", {}, "text", ["one", "two", "three", "four"])
+ );
+ properties.toPropertyMap();
+ Assert.deepEqual(properties.getFirstValue("org"), [
+ "one",
+ "two",
+ "three",
+ "four",
+ ]);
+});
+
+/**
+ * Tests adding to and removing from VCardProperties using VCardPropertyEntry.
+ */
+add_task(function testEntryMethods() {
+ // Sanity check.
+
+ let props = new VCardProperties();
+ Assert.deepEqual(props.entries, [], "props has no entries");
+
+ // Add property entries.
+
+ // Real VCardPropertyEntry objects.
+ let charlie = new VCardPropertyEntry(
+ "email",
+ { type: "home" },
+ "text",
+ "charlie@invalid"
+ );
+ let delta = new VCardPropertyEntry(
+ "email",
+ { type: "work" },
+ "text",
+ "delta@invalid"
+ );
+
+ // Ordinary objects for Assert.deepEqual comparison. Use these objects to be
+ // sure of the values being tested.
+ let data = {
+ charlie: {
+ name: "email",
+ params: { type: "home" },
+ type: "text",
+ value: "charlie@invalid",
+ },
+ delta: {
+ name: "email",
+ params: { type: "work" },
+ type: "text",
+ value: "delta@invalid",
+ },
+ juliet: {
+ name: "email",
+ params: { type: "home" },
+ type: "text",
+ value: "juliet@invalid",
+ },
+ };
+
+ Assert.ok(props.addEntry(charlie));
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.charlie],
+ "props.email has one entry"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["charlie@invalid"],
+ "props.email has one value"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "charlie@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(props.entries, [data.charlie], "props has one entry");
+
+ Assert.ok(props.addEntry(delta));
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.charlie, data.delta],
+ "props.email has two entries"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["charlie@invalid", "delta@invalid"],
+ "props.email has two values"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "charlie@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(
+ props.entries,
+ [data.charlie, data.delta],
+ "props has two entries"
+ );
+
+ Assert.ok(!props.addEntry(charlie));
+ propertyArrayEqual(
+ props.entries,
+ [data.charlie, data.delta],
+ "props still has two entries"
+ );
+
+ // Update a property entry.
+
+ charlie.value = "juliet@invalid";
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.juliet, data.delta],
+ "props.email has two entries"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["juliet@invalid", "delta@invalid"],
+ "props.email has two values"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "juliet@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(
+ props.entries,
+ [data.juliet, data.delta],
+ "props has two entries"
+ );
+
+ // Clone a property entry.
+
+ let juliet = charlie.clone();
+ Assert.notEqual(
+ juliet,
+ charlie,
+ "cloned VCardPropertyEntry is not the same object"
+ );
+ propertyEqual(
+ juliet,
+ data.juliet,
+ "cloned VCardPropertyEntry has the same properties"
+ );
+
+ // Delete a property entry.
+
+ Assert.ok(props.removeEntry(delta));
+ propertyArrayEqual(
+ props.getAllEntries("email"),
+ [data.juliet],
+ "props.email has one entry"
+ );
+ Assert.deepEqual(
+ props.getAllValues("email"),
+ ["juliet@invalid"],
+ "props.email has one value"
+ );
+ Assert.equal(
+ props.getFirstValue("email"),
+ "juliet@invalid",
+ "props.email has a first value"
+ );
+ propertyArrayEqual(props.entries, [data.juliet], "props has one entry");
+
+ // Delete a property entry using a clone of it.
+
+ Assert.ok(props.removeEntry(juliet));
+ propertyArrayEqual(props.entries, [], "all entries removed");
+});
+
+/**
+ * Tests adding to and removing from VCardProperties using names and values.
+ * Uses the vCard 3 default entry types.
+ */
+add_task(function testValueMethods3() {
+ let props = new VCardProperties();
+
+ // Add a value.
+
+ let first = props.addValue("tel", "1234567");
+ propertyEqual(first, {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "1234567",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ ]);
+
+ // Add a second value.
+
+ let second = props.addValue("tel", "2345678");
+ propertyEqual(second, {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "2345678",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "2345678" },
+ ]);
+
+ // Add a value that already exists. The existing property should be returned.
+
+ let secondCopy = props.addValue("tel", "2345678");
+ Assert.equal(secondCopy, second);
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "2345678" },
+ ]);
+
+ // Add a third value.
+
+ let third = props.addValue("tel", "3456789");
+ propertyEqual(third, {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "3456789",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "2345678" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove the second value.
+
+ props.removeValue("tel", "2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove a value that's already been removed.
+
+ props.removeValue("tel", "2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove a value that never existed.
+
+ props.removeValue("tel", "4567890");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "1234567" },
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove the first value.
+
+ props.removeValue("tel", "1234567");
+ propertyArrayEqual(props.entries, [
+ { name: "tel", params: {}, type: "phone-number", value: "3456789" },
+ ]);
+
+ // Remove the last value.
+
+ props.removeValue("tel", "3456789");
+ propertyArrayEqual(props.entries, []);
+});
+
+/**
+ * Tests adding to and removing from VCardProperties using names and values.
+ * Uses the vCard 4 default entry types.
+ */
+add_task(function testValueMethods4() {
+ let props = new VCardProperties("4.0");
+
+ // Add a value.
+
+ let first = props.addValue("tel", "tel:1234567");
+ propertyEqual(first, {
+ name: "tel",
+ params: {},
+ type: "uri",
+ value: "tel:1234567",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ ]);
+
+ // Add a second value.
+
+ let second = props.addValue("tel", "tel:2345678");
+ propertyEqual(second, {
+ name: "tel",
+ params: {},
+ type: "uri",
+ value: "tel:2345678",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:2345678" },
+ ]);
+
+ // Add a value that already exists. The existing property should be returned.
+
+ let secondCopy = props.addValue("tel", "tel:2345678");
+ Assert.equal(secondCopy, second);
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:2345678" },
+ ]);
+
+ // Add a third value.
+
+ let third = props.addValue("tel", "tel:3456789");
+ propertyEqual(third, {
+ name: "tel",
+ params: {},
+ type: "uri",
+ value: "tel:3456789",
+ });
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:2345678" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove the second value.
+
+ props.removeValue("tel", "tel:2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove a value that's already been removed.
+
+ props.removeValue("tel", "tel:2345678");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove a value that never existed.
+
+ props.removeValue("tel", "tel:4567890");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:1234567" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove the first value.
+
+ props.removeValue("tel", "tel:1234567");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ { name: "tel", params: {}, type: "uri", value: "tel:3456789" },
+ ]);
+
+ // Remove the last value.
+
+ props.removeValue("tel", "tel:3456789");
+ propertyArrayEqual(props.entries, [
+ { name: "version", params: {}, type: "text", value: "4.0" },
+ ]);
+});
+
+/**
+ * Tests retrieving entries and values in preference order.
+ */
+add_task(function testSortMethods() {
+ let props = new VCardProperties();
+ props.addEntry(new VCardPropertyEntry("email", {}, "text", "third@invalid"));
+ props.addEntry(
+ new VCardPropertyEntry("email", { pref: 2 }, "text", "second@invalid")
+ );
+ props.addEntry(new VCardPropertyEntry("email", {}, "text", "fourth@invalid"));
+ props.addEntry(
+ new VCardPropertyEntry("email", { pref: 1 }, "text", "first@invalid")
+ );
+
+ propertyArrayEqual(props.getAllEntriesSorted("email"), [
+ {
+ name: "email",
+ params: { pref: 1 },
+ type: "text",
+ value: "first@invalid",
+ },
+ {
+ name: "email",
+ params: { pref: 2 },
+ type: "text",
+ value: "second@invalid",
+ },
+ { name: "email", params: {}, type: "text", value: "third@invalid" },
+ { name: "email", params: {}, type: "text", value: "fourth@invalid" },
+ ]);
+
+ Assert.deepEqual(props.getAllValuesSorted("email"), [
+ "first@invalid",
+ "second@invalid",
+ "third@invalid",
+ "fourth@invalid",
+ ]);
+});
+
+/**
+ * Tests the `clone` method of VCardProperties.
+ */
+add_task(function testClone() {
+ let properties = VCardProperties.fromVCard(
+ formatVCard`
+ BEGIN:VCARD
+ FN:this is a test
+ N:test;this;is,a;;
+ EMAIL;PREF=1;TYPE=WORK:test@invalid
+ EMAIL:test@test.invalid
+ END:VCARD`
+ );
+ let clone = properties.clone();
+
+ Assert.deepEqual(clone.entries, properties.entries);
+ Assert.notEqual(clone.entries, properties.entries);
+
+ for (let i = 0; i < 4; i++) {
+ Assert.deepEqual(clone.entries[i].value, properties.entries[i].value);
+ Assert.notEqual(clone.entries[i], properties.entries[i]);
+ Assert.ok(clone.entries[i].equals(properties.entries[i]));
+ }
+
+ Assert.equal(clone.toVCard(), properties.toVCard());
+});
+
+/**
+ * Tests that entries with a group prefix are correctly handled, and the
+ * `getGroupedEntries` method of VCardProperties.
+ */
+add_task(function testGroupEntries() {
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ GROUP1.FN:test
+ GROUP1.X-FOO:bar
+ NOTE:this doesn't have a group
+ END:VCARD`;
+
+ let properties = VCardProperties.fromVCard(vCard);
+
+ let data = [
+ {
+ name: "fn",
+ params: {
+ group: "group1",
+ },
+ type: "text",
+ value: "test",
+ },
+ {
+ name: "x-foo",
+ params: {
+ group: "group1",
+ },
+ type: "unknown",
+ value: "bar",
+ },
+ {
+ name: "note",
+ params: {},
+ type: "text",
+ value: "this doesn't have a group",
+ },
+ ];
+
+ propertyArrayEqual(properties.entries, data);
+ Assert.equal(properties.toVCard(), vCard);
+ propertyArrayEqual(properties.getGroupedEntries("group1"), data.slice(0, 2));
+
+ let clone = properties.clone();
+ propertyArrayEqual(clone.entries, data);
+ Assert.equal(clone.toVCard(), vCard);
+ propertyArrayEqual(clone.getGroupedEntries("group1"), data.slice(0, 2));
+});
+
+/**
+ * Tests that we correctly fix Google's bad escaping of colons in values, and
+ * other characters in URI values.
+ */
+add_task(function testGoogleEscaping() {
+ let vCard = formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ N:test;en\\\\c\\:oding;;;
+ FN:en\\\\c\\:oding test
+ TITLE:title\\:title\\;title\\,title\\\\title\\\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\
+ TEL:tel\\:0123\\\\4567
+ EMAIL:test\\\\test@invalid
+ NOTE:notes\\:\\nnotes\\;\\nnotes\\,\\nnotes\\\\
+ URL:http\\://host/url\\:url\\;url\\,url\\\\url
+ END:VCARD`;
+
+ let goodVCard = formatVCard`
+ BEGIN:VCARD
+ VERSION:3.0
+ N:test;en\\\\c:oding;;;
+ FN:en\\\\c:oding test
+ TITLE:title:title\\;title\\,title\\\\title\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\
+ TEL:tel:01234567
+ EMAIL:test\\\\test@invalid
+ NOTE:notes:\\nnotes\\;\\nnotes\\,\\nnotes\\\\
+ URL:http://host/url:url;url,url\\url
+ END:VCARD`;
+
+ let data = [
+ {
+ name: "version",
+ params: {},
+ type: "text",
+ value: "3.0",
+ },
+ {
+ name: "n",
+ params: {},
+ type: "text",
+ value: ["test", "en\\c:oding", "", "", ""],
+ },
+ {
+ name: "fn",
+ params: {},
+ type: "text",
+ value: "en\\c:oding test",
+ },
+ {
+ name: "title",
+ params: {},
+ type: "text",
+ value: "title:title;title,title\\title\\:title\\;title\\,title\\\\",
+ },
+ {
+ name: "tel",
+ params: {},
+ type: "phone-number",
+ value: "tel:01234567",
+ },
+ {
+ name: "email",
+ params: {},
+ type: "text",
+ value: "test\\test@invalid",
+ },
+ {
+ name: "note",
+ params: {},
+ type: "text",
+ value: "notes:\nnotes;\nnotes,\nnotes\\",
+ },
+ {
+ name: "url",
+ params: {},
+ type: "uri",
+ value: "http://host/url:url;url,url\\url",
+ },
+ ];
+
+ let properties = VCardProperties.fromVCard(vCard, { isGoogleCardDAV: true });
+ propertyArrayEqual(properties.entries, data);
+ Assert.equal(properties.toVCard(), goodVCard);
+
+ let goodProperties = VCardProperties.fromVCard(goodVCard);
+ propertyArrayEqual(goodProperties.entries, data);
+ Assert.equal(goodProperties.toVCard(), goodVCard);
+});
diff --git a/comm/mailnews/addrbook/test/unit/xpcshell.ini b/comm/mailnews/addrbook/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..2701be7a7a
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/xpcshell.ini
@@ -0,0 +1,60 @@
+[DEFAULT]
+head = head.js
+support-files = data/*
+tags = addrbook
+
+[test_abCardProperty.js]
+[test_addrBookCard.js]
+[test_basic_nsIAbDirectory.js]
+[test_bug387403.js]
+tags = addrbook vcard
+[test_bug448165.js]
+[test_bug534822.js]
+[test_bug1522453.js]
+[test_bug1769889.js]
+tags = addrbook vcard
+[test_cardForEmail.js]
+[test_collection.js]
+[test_collection_2.js]
+[test_convertOnSave.js]
+tags = addrbook vcard
+[test_db_enumerator.js]
+[test_delete_book.js]
+[test_export.js]
+tags = addrbook vcard
+[test_jsaddrbook.js]
+[test_LDAPMessage.js]
+[test_LDAPSyncQuery.js]
+[test_ldap1.js]
+[test_ldap2.js]
+[test_ldapOffline.js]
+[test_ldapquery.js]
+[test_ldapReplication.js]
+skip-if = debug # Fails for unknown reasons.
+[test_mailList1.js]
+[test_nsAbAutoCompleteMyDomain.js]
+[test_nsAbAutoCompleteSearch1.js]
+[test_nsAbAutoCompleteSearch2.js]
+[test_nsAbAutoCompleteSearch3.js]
+[test_nsAbAutoCompleteSearch4.js]
+[test_nsAbAutoCompleteSearch5.js]
+[test_nsAbAutoCompleteSearch6.js]
+[test_nsAbAutoCompleteSearch7.js]
+[test_nsAbManager2.js]
+[test_nsAbManager3.js]
+[test_nsAbManager4.js]
+[test_nsAbManager5.js]
+[test_nsAbManager6.js]
+[test_nsIAbCard.js]
+tags = addrbook vcard
+[test_nsIAbDirectory_getMailListFromName.js]
+[test_nsLDAPURL.js]
+[test_photoURL.js]
+[test_preferDisplayName.js]
+[test_search.js]
+[test_vCard.js]
+tags = addrbook vcard
+[test_vCard21.js]
+tags = addrbook vcard
+[test_vCardProperties.js]
+tags = addrbook vcard
diff --git a/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini b/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini
new file mode 100644
index 0000000000..720607fbe0
--- /dev/null
+++ b/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+head = head_cardDAV.js
+tags = addrbook carddav vcard
+prefs =
+ carddav.setup.loglevel=Debug
+ carddav.sync.loglevel=Debug
+
+[test_cardDAV_copyCard.js]
+[test_cardDAV_offline.js]
+[test_cardDAV_serverModified.js]
+[test_cardDAV_syncV1.js]
+[test_cardDAV_syncV2.js]