summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/addrbook/test/unit/test_ldapquery.js
blob: 90b1f1673d0fecc6f7ef4de9a0a223fb66b50474 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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();
});