Bug 1608304 part 2 - Create a mock LDAP server and some tests; r=mkmelin

--HG--
extra : rebase_source : 2f9dc90f9e48b6d4c16c94e21bbec030f1c872f4
extra : histedit_source : 351a811ea0a92e6f575068847b39d3a46579f61e%2C85d9621b83f5f9dc217d572fc215e27915a9e76f
This commit is contained in:
Geoff Lankow 2020-01-10 12:46:24 +13:00
Родитель 2a041ddaab
Коммит decc05d54d
8 изменённых файлов: 648 добавлений и 0 удалений

Просмотреть файл

@ -142,6 +142,7 @@ var gAddressBookAbViewListener = {
},
onCountChanged(total) {
SetStatusText(total);
window.dispatchEvent(new CustomEvent("countchange"));
},
};

Просмотреть файл

@ -11,4 +11,6 @@ prefs =
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]

Просмотреть файл

@ -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");
});

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -3,6 +3,10 @@
# 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',

Просмотреть файл

@ -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"
}
}
}

Просмотреть файл

@ -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",
]);
});

Просмотреть файл

@ -17,6 +17,8 @@ support-files = data/*
[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]