Bug 1608304 part 2 - Create a mock LDAP server and some tests; r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Fri, 10 Jan 2020 12:46:24 +1300
changeset 37951 f39320031868079d2bcab28327cafd1dc8f3329b
parent 37950 d85625eb94fbacbcbb6d442ed66039e709cc3708
child 37952 46ed6cdc30b5f1fa6e87e0334008fa9b537f5624
push id398
push userclokep@gmail.com
push dateMon, 09 Mar 2020 19:10:28 +0000
reviewersmkmelin
bugs1608304
Bug 1608304 part 2 - Create a mock LDAP server and some tests; r=mkmelin
mail/components/addrbook/content/addressbook.js
mail/components/addrbook/test/browser/browser.ini
mail/components/addrbook/test/browser/browser_ldap_search.js
mailnews/addrbook/test/LDAPServer.jsm
mailnews/addrbook/test/moz.build
mailnews/addrbook/test/unit/data/ldap_contacts.json
mailnews/addrbook/test/unit/test_ldapReplication.js
mailnews/addrbook/test/unit/xpcshell.ini
--- a/mail/components/addrbook/content/addressbook.js
+++ b/mail/components/addrbook/content/addressbook.js
@@ -137,16 +137,17 @@ function OnUnloadAddressBook() {
 }
 
 var gAddressBookAbViewListener = {
   onSelectionChanged() {
     ResultsPaneSelectionChanged();
   },
   onCountChanged(total) {
     SetStatusText(total);
+    window.dispatchEvent(new CustomEvent("countchange"));
   },
 };
 
 function GetAbViewListener() {
   return gAddressBookAbViewListener;
 }
 
 // we won't show the window until the onload() handler is finished
--- a/mail/components/addrbook/test/browser/browser.ini
+++ b/mail/components/addrbook/test/browser/browser.ini
@@ -6,9 +6,11 @@ prefs =
   ldap_2.servers.osx.uri=
   mail.provider.suppress_dialog_on_startup=true
   mail.spotlight.firstRunDone=true
   mail.winsearch.firstRunDone=true
   mailnews.start_page.override_url=about:blank
   mailnews.start_page.url=about:blank
 subsuite = thunderbird
 
+[browser_ldap_search.js]
+support-files = ../../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json
 [browser_mailing_lists.js]
new file mode 100644
--- /dev/null
+++ b/mail/components/addrbook/test/browser/browser_ldap_search.js
@@ -0,0 +1,94 @@
+/* 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 { mailTestUtils } = ChromeUtils.import(
+  "resource://testing-common/mailnews/mailTestUtils.js"
+);
+
+const jsonFile =
+  "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/ldap_contacts.json";
+
+add_task(async () => {
+  LDAPServer.open();
+  let response = await fetch(jsonFile);
+  let ldapContacts = await response.json();
+
+  let bookPref = MailServices.ab.newAddressBook(
+    "Mochitest",
+    `ldap://localhost:${LDAPServer.port}/`,
+    0
+  );
+  let book = MailServices.ab.getDirectoryFromId(bookPref);
+
+  let abWindow = await openAddressBookWindow();
+  let abDocument = abWindow.document;
+
+  registerCleanupFunction(() => {
+    abWindow.close();
+    MailServices.ab.deleteAddressBook(book.URI);
+    LDAPServer.close();
+  });
+
+  let dirTree = abDocument.getElementById("dirTree");
+  is(dirTree.view.getCellText(2, dirTree.columns[0]), "Mochitest");
+  mailTestUtils.treeClick(EventUtils, abWindow, dirTree, 2, 0, {});
+
+  let resultsTree = abDocument.getElementById("abResultsTree");
+
+  let searchBox = abDocument.getElementById("peopleSearchInput");
+  EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+  EventUtils.sendString("holmes", abWindow);
+
+  await LDAPServer.read(); // BindRequest
+  is(resultsTree.view.rowCount, 0);
+  LDAPServer.writeBindResponse();
+
+  await LDAPServer.read(); // SearchRequest
+  LDAPServer.writeSearchResultEntry(ldapContacts.mycroft);
+  LDAPServer.writeSearchResultEntry(ldapContacts.sherlock);
+  LDAPServer.writeSearchResultDone();
+
+  await new Promise(resolve => {
+    abWindow.addEventListener("countchange", function onCountChange() {
+      if (resultsTree.view && resultsTree.view.rowCount == 2) {
+        abWindow.removeEventListener("countchange", onCountChange);
+        resolve();
+      }
+    });
+  });
+
+  is(resultsTree.view.rowCount, 2);
+  is(resultsTree.view.getCellText(0, resultsTree.columns[0]), "Mycroft Holmes");
+  is(
+    resultsTree.view.getCellText(1, resultsTree.columns[0]),
+    "Sherlock Holmes"
+  );
+
+  EventUtils.synthesizeMouseAtCenter(searchBox, {}, abWindow);
+  EventUtils.synthesizeKey("a", { accelKey: true }, abWindow);
+  EventUtils.sendString("john", abWindow);
+
+  await LDAPServer.read(); // BindRequest
+  is(resultsTree.view.rowCount, 0);
+  LDAPServer.writeBindResponse();
+
+  await LDAPServer.read(); // SearchRequest
+  LDAPServer.writeSearchResultEntry(ldapContacts.john);
+  LDAPServer.writeSearchResultDone();
+
+  await new Promise(resolve => {
+    abWindow.addEventListener("countchange", function onCountChange() {
+      if (resultsTree.view && resultsTree.view.rowCount == 1) {
+        abWindow.removeEventListener("countchange", onCountChange);
+        resolve();
+      }
+    });
+  });
+
+  is(resultsTree.view.rowCount, 1);
+  is(resultsTree.view.getCellText(0, resultsTree.columns[0]), "John Watson");
+});
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/LDAPServer.jsm
@@ -0,0 +1,280 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * 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 = {
+  serverSocket: null,
+
+  QueryInterface: ChromeUtils.generateQI([
+    Ci.nsIInputStreamCallback,
+    Ci.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.
+   */
+  read() {
+    return new Promise(resolve => {
+      if (this._data) {
+        resolve(this._data);
+        delete this._data;
+      }
+      this._inputStreamReadyResolve = resolve;
+    });
+  },
+  /**
+   * Sends raw data to the application. Generally this shouldn't be used
+   * directly but it may be useful for testing.
+   *
+   * @param {byte array} Data
+   */
+  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} An object representing a person. Keys of the object are:
+   *                 - dn         The LDAP DN of the person
+   *                 - 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 section 4.5.2 of the RFC.
+   */
+  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(" ")
+      );
+    }
+    this._lastMessageID = data[4];
+
+    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;
+}
--- a/mailnews/addrbook/test/moz.build
+++ b/mailnews/addrbook/test/moz.build
@@ -1,9 +1,13 @@
 # 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 += [
+    'LDAPServer.jsm',
+]
+
 XPCSHELL_TESTS_MANIFESTS += [
     'unit/xpcshell.ini',
     'unit/xpcshell_migration.ini',
 ]
new file mode 100644
--- /dev/null
+++ b/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"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/addrbook/test/unit/test_ldapReplication.js
@@ -0,0 +1,161 @@
+/* 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 { OS } = ChromeUtils.import("resource://gre/modules/osfile.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 contents = await OS.File.read(jsonFile.path);
+  let ldapContacts = await JSON.parse(new TextDecoder().decode(contents));
+
+  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(); // BindRequest
+  LDAPServer.writeBindResponse();
+
+  await LDAPServer.read(); // 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(); // BindRequest
+  LDAPServer.writeBindResponse();
+
+  await LDAPServer.read(); // 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(); // BindRequest
+  LDAPServer.writeBindResponse();
+
+  await LDAPServer.read(); // 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",
+  ]);
+});
--- a/mailnews/addrbook/test/unit/xpcshell.ini
+++ b/mailnews/addrbook/test/unit/xpcshell.ini
@@ -12,16 +12,18 @@ support-files = data/*
 [test_collection.js]
 [test_collection_2.js]
 [test_db_enumerator.js]
 [test_jsaddrbook.js]
 [test_jsaddrbook_inner.js]
 [test_ldap1.js]
 [test_ldap2.js]
 [test_ldapOffline.js]
+[test_ldapReplication.js]
+skip-if = debug # Fails for unknown reasons.
 [test_mailList1.js]
 [test_notifications.js]
 [test_nsAbAutoCompleteMyDomain.js]
 [test_nsAbAutoCompleteSearch1.js]
 [test_nsAbAutoCompleteSearch2.js]
 [test_nsAbAutoCompleteSearch3.js]
 [test_nsAbAutoCompleteSearch4.js]
 [test_nsAbAutoCompleteSearch5.js]