'use strict'; const util = require('util'); const tap = require('tap'); const uuid = require('uuid'); const vasync = require('vasync'); const { getSock } = require('./utils'); const ldap = require('../lib'); const { Attribute, Change } = ldap; const SUFFIX = 'dc=test'; const LDAP_CONNECT_TIMEOUT = process.env.LDAP_CONNECT_TIMEOUT || 0; const BIND_DN = 'cn=root'; const BIND_PW = 'secret'; tap.beforeEach((done, t) => { t.context.socketPath = getSock(); t.context.server = ldap.createServer(); const server = t.context.server; server.bind(BIND_DN, function (req, res, next) { if (req.credentials !== BIND_PW) return next(new ldap.InvalidCredentialsError('Invalid password')); res.end(); return next(); }); server.add(SUFFIX, function (req, res, next) { res.end(); return next(); }); server.compare(SUFFIX, function (req, res, next) { res.end(req.value === 'test'); return next(); }); server.del(SUFFIX, function (req, res, next) { res.end(); return next(); }); // LDAP whoami server.exop('1.3.6.1.4.1.4203.1.11.3', function (req, res, next) { res.value = 'u:xxyyz@EXAMPLE.NET'; res.end(); return next(); }); server.modify(SUFFIX, function (req, res, next) { res.end(); return next(); }); server.modifyDN(SUFFIX, function (req, res, next) { res.end(); return next(); }); server.search('dc=slow', function (req, res, next) { res.send({ dn: 'dc=slow', attributes: { 'you': 'wish', 'this': 'was', 'faster': '.' } }); setTimeout(function () { res.end(); next(); }, 250); }); server.search('dc=timeout', function (req, res, next) { // Cause the client to timeout by not sending a response. }); server.search(SUFFIX, function (req, res, next) { if (req.dn.equals('cn=ref,' + SUFFIX)) { res.send(res.createSearchReference('ldap://localhost')); } else if (req.dn.equals('cn=bin,' + SUFFIX)) { res.send(res.createSearchEntry({ objectName: req.dn, attributes: { 'foo;binary': 'wr0gKyDCvCA9IMK+', 'gb18030': Buffer.from([0xB5, 0xE7, 0xCA, 0xD3, 0xBB, 0xFA]), 'objectclass': 'binary' } })); } else { var e = res.createSearchEntry({ objectName: req.dn, attributes: { cn: ['unit', 'test'], SN: 'testy' } }); res.send(e); res.send(e); } res.end(); return next(); }); server.search('cn=sizelimit', function (req, res, next) { const sizeLimit = 200; for (let i = 0; i < 1000; i++) { if (req.sizeLimit > 0 && i >= req.sizeLimit) { break; } else if (i > sizeLimit) { res.end(ldap.LDAP_SIZE_LIMIT_EXCEEDED); return next(); } res.send({ dn: util.format('o=%d, cn=sizelimit', i), attributes: { o: [i], objectclass: ['pagedResult'] } }); } res.end(); return next(); }); server.search('cn=paged', function (req, res, next) { const min = 0; const max = 1000; function sendResults(start, end) { start = (start < min) ? min : start; end = (end > max || end < min) ? max : end; let i; for (i = start; i < end; i++) { res.send({ dn: util.format('o=%d, cn=paged', i), attributes: { o: [i], objectclass: ['pagedResult'] } }); } return i; } let cookie = null; let pageSize = 0; req.controls.forEach(function (control) { if (control.type === ldap.PagedResultsControl.OID) { pageSize = control.value.size; cookie = control.value.cookie; } }); if (cookie && Buffer.isBuffer(cookie)) { // Do simple paging let first = min; if (cookie.length !== 0) { first = parseInt(cookie.toString(), 10); } const last = sendResults(first, first + pageSize); let resultCookie; if (last < max) { resultCookie = Buffer.from(last.toString()); } else { resultCookie = Buffer.from(''); } res.controls.push(new ldap.PagedResultsControl({ value: { size: pageSize, // correctness not required here cookie: resultCookie } })); res.end(); next(); } else { // don't allow non-paged searches for this test endpoint next(new ldap.UnwillingToPerformError()); } }); server.search('cn=pagederr', function (req, res, next) { let cookie = null; req.controls.forEach(function (control) { if (control.type === ldap.PagedResultsControl.OID) { cookie = control.value.cookie; } }); if (cookie && Buffer.isBuffer(cookie) && cookie.length === 0) { // send first "page" res.send({ dn: util.format('o=result, cn=pagederr'), attributes: { o: 'result', objectclass: ['pagedResult'] } }); res.controls.push(new ldap.PagedResultsControl({ value: { size: 2, cookie: Buffer.from('a') } })); res.end(); return next(); } else { // send error instead of second page res.end(ldap.LDAP_SIZE_LIMIT_EXCEEDED); return next(); } }); server.search('dc=empty', function (req, res, next) { res.send({ dn: 'dc=empty', attributes: { member: [], 'member;range=0-1': ['cn=user1, dc=empty', 'cn=user2, dc=empty'] } }); res.end(); return next(); }); server.search('cn=busy', function (req, res, next) { next(new ldap.BusyError('too much to do')); }); server.search('', function (req, res, next) { if (req.dn.toString() === '') { res.send({ dn: '', attributes: { objectclass: ['RootDSE', 'top'] } }); res.end(); } else { // Turn away any other requests (since '' is the fallthrough route) res.errorMessage = 'No tree found for: ' + req.dn.toString(); res.end(ldap.LDAP_NO_SUCH_OBJECT); } return next(); }); server.unbind(function (req, res, next) { res.end(); return next(); }); server.listen(t.context.socketPath, function () { const client = ldap.createClient({ connectTimeout: parseInt(LDAP_CONNECT_TIMEOUT, 10), socketPath: t.context.socketPath }); t.context.client = client; client.on('connect', () => done()) }) }) tap.afterEach((done, t) => { t.context.client.unbind((err) => { t.error(err); t.context.server.close(() => done()); }) }) tap.test('simple bind failure', function (t) { t.context.client.bind(BIND_DN, uuid(), function (err, res) { t.ok(err); t.notOk(res); t.ok(err instanceof ldap.InvalidCredentialsError); t.ok(err instanceof Error); t.ok(err.dn); t.ok(err.message); t.ok(err.stack); t.end(); }); }); tap.test('simple bind success', function (t) { t.context.client.bind(BIND_DN, BIND_PW, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('simple anonymous bind (empty credentials)', function (t) { t.context.client.bind('', '', function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('auto-bind bad credentials', function (t) { const clt = ldap.createClient({ socketPath: t.context.socketPath, bindDN: BIND_DN, bindCredentials: 'totallybogus' }); clt.once('error', function (err) { t.equal(err.code, ldap.LDAP_INVALID_CREDENTIALS); t.ok(clt._socket.destroyed, 'expect socket to be destroyed'); clt.destroy(); t.end(); }); }); tap.test('auto-bind success', function (t) { const clt = ldap.createClient({ socketPath: t.context.socketPath, bindDN: BIND_DN, bindCredentials: BIND_PW }); clt.once('connect', function () { t.ok(clt); clt.destroy(); t.end(); }); }); tap.test('add success', function (t) { const attrs = [ new Attribute({ type: 'cn', vals: ['test'] }) ]; t.context.client.add('cn=add, ' + SUFFIX, attrs, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('add success with object', function (t) { const entry = { cn: ['unit', 'add'], sn: 'test' }; t.context.client.add('cn=add, ' + SUFFIX, entry, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('compare success', function (t) { t.context.client.compare('cn=compare, ' + SUFFIX, 'cn', 'test', function (err, matched, res) { t.error(err); t.ok(matched); t.ok(res); t.end(); }); }); tap.test('compare false', function (t) { t.context.client.compare('cn=compare, ' + SUFFIX, 'cn', 'foo', function (err, matched, res) { t.error(err); t.notOk(matched); t.ok(res); t.end(); }); }); tap.test('compare bad suffix', function (t) { t.context.client.compare('cn=' + uuid(), 'cn', 'foo', function (err, matched, res) { t.ok(err); t.ok(err instanceof ldap.NoSuchObjectError); t.notOk(matched); t.notOk(res); t.end(); }); }); tap.test('delete success', function (t) { t.context.client.del('cn=delete, ' + SUFFIX, function (err, res) { t.error(err); t.ok(res); t.end(); }); }); tap.test('delete with control (GH-212)', function (t) { const control = new ldap.Control({ type: '1.2.3.4', criticality: false }); t.context.client.del('cn=delete, ' + SUFFIX, control, function (err, res) { t.error(err); t.ok(res); t.end(); }); }); tap.test('exop success', function (t) { t.context.client.exop('1.3.6.1.4.1.4203.1.11.3', function (err, value, res) { t.error(err); t.ok(value); t.ok(res); t.equal(value, 'u:xxyyz@EXAMPLE.NET'); t.end(); }); }); tap.test('exop invalid', function (t) { t.context.client.exop('1.2.3.4', function (err, res) { t.ok(err); t.ok(err instanceof ldap.ProtocolError); t.notOk(res); t.end(); }); }); tap.test('bogus exop (GH-17)', function (t) { t.context.client.exop('cn=root', function (err) { t.ok(err); t.end(); }); }); tap.test('modify success', function (t) { const change = new Change({ type: 'Replace', modification: new Attribute({ type: 'cn', vals: ['test'] }) }); t.context.client.modify('cn=modify, ' + SUFFIX, change, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('modify change plain object success', function (t) { const change = new Change({ type: 'Replace', modification: { cn: 'test' } }); t.context.client.modify('cn=modify, ' + SUFFIX, change, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); // https://github.com/ldapjs/node-ldapjs/pull/435 tap.test('can delete attributes', function (t) { const change = new Change({ type: 'Delete', modification: { cn: null } }); t.context.client.modify('cn=modify,' + SUFFIX, change, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('modify array success', function (t) { const changes = [ new Change({ operation: 'Replace', modification: new Attribute({ type: 'cn', vals: ['test'] }) }), new Change({ operation: 'Delete', modification: new Attribute({ type: 'sn' }) }) ]; t.context.client.modify('cn=modify, ' + SUFFIX, changes, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('modify change plain object success (GH-31)', function (t) { const change = { type: 'replace', modification: { cn: 'test', sn: 'bar' } }; t.context.client.modify('cn=modify, ' + SUFFIX, change, function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('modify DN new RDN only', function (t) { t.context.client.modifyDN('cn=old, ' + SUFFIX, 'cn=new', function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('modify DN new superior', function (t) { t.context.client.modifyDN('cn=old, ' + SUFFIX, 'cn=new, dc=foo', function (err, res) { t.error(err); t.ok(res); t.equal(res.status, 0); t.end(); }); }); tap.test('search basic', function (t) { t.context.client.search('cn=test, ' + SUFFIX, '(objectclass=*)', function (err, res) { t.error(err); t.ok(res); let gotEntry = 0; res.on('searchEntry', function (entry) { t.ok(entry); t.ok(entry instanceof ldap.SearchEntry); t.equal(entry.dn.toString(), 'cn=test, ' + SUFFIX); t.ok(entry.attributes); t.ok(entry.attributes.length); t.equal(entry.attributes[0].type, 'cn'); t.equal(entry.attributes[1].type, 'SN'); t.ok(entry.object); gotEntry++; }); res.on('error', function (err) { t.fail(err); }); res.on('end', function (res) { t.ok(res); t.ok(res instanceof ldap.SearchResponse); t.equal(res.status, 0); t.equal(gotEntry, 2); t.end(); }); }); }); tap.test('search sizeLimit', function (t) { t.test('over limit', function (t2) { t.context.client.search('cn=sizelimit', {}, function (err, res) { t2.error(err); res.on('error', function (error) { t2.equal(error.name, 'SizeLimitExceededError'); t2.end(); }); }); }); t.test('under limit', function (t2) { const limit = 100; t.context.client.search('cn=sizelimit', {sizeLimit: limit}, function (err, res) { t2.error(err); let count = 0; res.on('searchEntry', function (entry) { count++; }); res.on('end', function () { t2.pass(); t2.equal(count, limit); t2.end(); }); res.on('error', t2.error.bind(t)); }); }); t.end() }); tap.test('search paged', { timeout: 10000 }, function (t) { t.test('paged - no pauses', function (t2) { let countEntries = 0; let countPages = 0; t.context.client.search('cn=paged', {paged: {pageSize: 100}}, function (err, res) { t2.error(err); res.on('searchEntry', entryListener); res.on('page', pageListener); res.on('error', (err) => t2.error(err)); res.on('end', function () { t2.equal(countEntries, 1000); t2.equal(countPages, 10); t2.end(); }); t2.tearDown(() => { res.removeListener('searchEntry', entryListener); res.removeListener('page', pageListener); }) function entryListener() { countEntries += 1; } function pageListener () { countPages += 1; } }); }); t.test('paged - pauses', function (t2) { let countPages = 0; t.context.client.search('cn=paged', { paged: { pageSize: 100, pagePause: true } }, function (err, res) { t2.error(err); res.on('page', pageListener); res.on('error', (err) => t2.error(err)); res.on('end', function () { t2.equal(countPages, 9); t2.end(); }); function pageListener (result, cb) { countPages++; // cancel after 9 to verify callback usage if (countPages === 9) { // another page should never be encountered res.removeListener('page', pageListener) .on('page', t2.fail.bind(null, 'unexpected page')); return cb(new Error()); } return cb(); } }); }); t.test('paged - no support (err handled)', function (t2) { t.context.client.search(SUFFIX, { paged: { pageSize: 100 } }, function (err, res) { t2.error(err); res.on('pageError', t2.ok.bind(t2)); res.on('end', function () { t2.pass(); t2.end(); }); }); }); t.test('paged - no support (err not handled)', function (t2) { t.context.client.search(SUFFIX, { paged: { pageSize: 100 } }, function (err, res) { t2.error(err); res.on('end', t2.fail.bind(t2)); res.on('error', function (error) { t2.ok(error); t2.end(); }); }); }); t.test('paged - redundant control', function (t2) { try { t.context.client.search(SUFFIX, { paged: { pageSize: 100 } }, new ldap.PagedResultsControl(), function (err, res) { t2.fail(); }); } catch (e) { t2.ok(e); t2.end(); } }); t.test('paged - handle later error', function (t2) { let countEntries = 0; let countPages = 0; t.context.client.search('cn=pagederr', { paged: { pageSize: 1 } }, function (err, res) { t2.error(err); res.on('searchEntry', function () { t2.ok(++countEntries); }); res.on('page', function () { t2.ok(++countPages); }); res.on('error', function (error) { t2.equal(countEntries, 1); t2.equal(countPages, 1); t2.end(); }); res.on('end', function () { t2.fail('should not be reached'); }); }); }); t.end(); }); tap.test('search referral', function (t) { t.context.client.search('cn=ref, ' + SUFFIX, '(objectclass=*)', function (err, res) { t.error(err); t.ok(res); let gotEntry = 0; let gotReferral = false; res.on('searchEntry', function (entry) { gotEntry++; }); res.on('searchReference', function (referral) { gotReferral = true; t.ok(referral); t.ok(referral instanceof ldap.SearchReference); t.ok(referral.uris); t.ok(referral.uris.length); }); res.on('error', function (err) { t.fail(err); }); res.on('end', function (res) { t.ok(res); t.ok(res instanceof ldap.SearchResponse); t.equal(res.status, 0); t.equal(gotEntry, 0); t.ok(gotReferral); t.end(); }); }); }); tap.test('search rootDSE', function (t) { t.context.client.search('', '(objectclass=*)', function (err, res) { t.error(err); t.ok(res); res.on('searchEntry', function (entry) { t.ok(entry); t.equal(entry.dn.toString(), ''); t.ok(entry.attributes); t.ok(entry.object); }); res.on('error', function (err) { t.fail(err); }); res.on('end', function (res) { t.ok(res); t.ok(res instanceof ldap.SearchResponse); t.equal(res.status, 0); t.end(); }); }); }); tap.test('search empty attribute', function (t) { t.context.client.search('dc=empty', '(objectclass=*)', function (err, res) { t.error(err); t.ok(res); let gotEntry = 0; res.on('searchEntry', function (entry) { var obj = entry.toObject(); t.equal('dc=empty', obj.dn); t.ok(obj.member); t.equal(obj.member.length, 0); t.ok(obj['member;range=0-1']); t.ok(obj['member;range=0-1'].length); gotEntry++; }); res.on('error', function (err) { t.fail(err); }); res.on('end', function (res) { t.ok(res); t.ok(res instanceof ldap.SearchResponse); t.equal(res.status, 0); t.equal(gotEntry, 1); t.end(); }); }); }); tap.test('GH-21 binary attributes', function (t) { t.context.client.search('cn=bin, ' + SUFFIX, '(objectclass=*)', function (err, res) { t.error(err); t.ok(res); let gotEntry = 0; const expect = Buffer.from('\u00bd + \u00bc = \u00be', 'utf8'); const expect2 = Buffer.from([0xB5, 0xE7, 0xCA, 0xD3, 0xBB, 0xFA]); res.on('searchEntry', function (entry) { t.ok(entry); t.ok(entry instanceof ldap.SearchEntry); t.equal(entry.dn.toString(), 'cn=bin, ' + SUFFIX); t.ok(entry.attributes); t.ok(entry.attributes.length); t.equal(entry.attributes[0].type, 'foo;binary'); t.equal(entry.attributes[0].vals[0], expect.toString('base64')); t.equal(entry.attributes[0].buffers[0].toString('base64'), expect.toString('base64')); t.ok(entry.attributes[1].type, 'gb18030'); t.equal(entry.attributes[1].buffers.length, 1); t.equal(expect2.length, entry.attributes[1].buffers[0].length); for (var i = 0; i < expect2.length; i++) t.equal(expect2[i], entry.attributes[1].buffers[0][i]); t.ok(entry.object); gotEntry++; }); res.on('error', function (err) { t.fail(err); }); res.on('end', function (res) { t.ok(res); t.ok(res instanceof ldap.SearchResponse); t.equal(res.status, 0); t.equal(gotEntry, 1); t.end(); }); }); }); tap.test('GH-23 case insensitive attribute filtering', function (t) { const opts = { filter: '(objectclass=*)', attributes: ['Cn'] }; t.context.client.search('cn=test, ' + SUFFIX, opts, function (err, res) { t.error(err); t.ok(res); let gotEntry = 0; res.on('searchEntry', function (entry) { t.ok(entry); t.ok(entry instanceof ldap.SearchEntry); t.equal(entry.dn.toString(), 'cn=test, ' + SUFFIX); t.ok(entry.attributes); t.ok(entry.attributes.length); t.equal(entry.attributes[0].type, 'cn'); t.ok(entry.object); gotEntry++; }); res.on('error', function (err) { t.fail(err); }); res.on('end', function (res) { t.ok(res); t.ok(res instanceof ldap.SearchResponse); t.equal(res.status, 0); t.equal(gotEntry, 2); t.end(); }); }); }); tap.test('GH-24 attribute selection of *', function (t) { const opts = { filter: '(objectclass=*)', attributes: ['*'] }; t.context.client.search('cn=test, ' + SUFFIX, opts, function (err, res) { t.error(err); t.ok(res); let gotEntry = 0; res.on('searchEntry', function (entry) { t.ok(entry); t.ok(entry instanceof ldap.SearchEntry); t.equal(entry.dn.toString(), 'cn=test, ' + SUFFIX); t.ok(entry.attributes); t.ok(entry.attributes.length); t.equal(entry.attributes[0].type, 'cn'); t.equal(entry.attributes[1].type, 'SN'); t.ok(entry.object); gotEntry++; }); res.on('error', function (err) { t.fail(err); }); res.on('end', function (res) { t.ok(res); t.ok(res instanceof ldap.SearchResponse); t.equal(res.status, 0); t.equal(gotEntry, 2); t.end(); }); }); }); tap.test('idle timeout', function (t) { t.context.client.idleTimeout = 250; function premature() { t.error(true); } t.context.client.on('idle', premature); t.context.client.search('dc=slow', 'objectclass=*', function (err, res) { t.error(err); res.on('searchEntry', function (res) { t.ok(res); }); res.on('error', function (err) { t.error(err); }); res.on('end', function () { var late = setTimeout(function () { t.error(false, 'too late'); }, 500); // It's ok to go idle now t.context.client.removeListener('idle', premature); t.context.client.on('idle', function () { clearTimeout(late); t.context.client.removeAllListeners('idle'); t.context.client.idleTimeout = 0; t.end(); }); }); }); }); tap.test('setup action', function (t) { const setupClient = ldap.createClient({ connectTimeout: parseInt(LDAP_CONNECT_TIMEOUT, 10), socketPath: t.context.socketPath }); setupClient.on('setup', function (clt, cb) { clt.bind(BIND_DN, BIND_PW, function (err, res) { t.error(err); cb(err); }); }); setupClient.search(SUFFIX, {scope: 'base'}, function (err, res) { t.error(err); t.ok(res); res.on('end', function () { setupClient.destroy(); t.end(); }); }); }); tap.test('setup reconnect', function (t) { const rClient = ldap.createClient({ connectTimeout: parseInt(LDAP_CONNECT_TIMEOUT, 10), socketPath: t.context.socketPath, reconnect: true }); rClient.on('setup', function (clt, cb) { clt.bind(BIND_DN, BIND_PW, function (err, res) { t.error(err); cb(err); }); }); function doSearch(_, cb) { rClient.search(SUFFIX, {scope: 'base'}, function (err, res) { t.error(err); res.on('end', function () { cb(); }); }); } vasync.pipeline({ funcs: [ doSearch, function cleanDisconnect(_, cb) { t.ok(rClient.connected); rClient.once('close', function (had_err) { t.error(had_err); t.equal(rClient.connected, false); cb(); }); rClient.unbind(); }, doSearch, function simulateError(_, cb) { const msg = 'fake socket error'; rClient.once('error', function (err) { t.equal(err.message, msg); t.ok(err); }); rClient.once('close', function (had_err) { // can't test had_err because the socket error is being faked cb(); }); rClient._socket.emit('error', new Error(msg)); }, doSearch ] }, function (err) { t.error(err); rClient.destroy(); t.end(); }); }); tap.test('setup abort', function (t) { const setupClient = ldap.createClient({ connectTimeout: parseInt(LDAP_CONNECT_TIMEOUT, 10), socketPath: t.context.socketPath, reconnect: true }); const message = "It's a trap!"; setupClient.on('setup', function (clt, cb) { // simulate failure t.ok(clt); cb(new Error(message)); }); setupClient.on('setupError', function (err) { t.ok(true); t.equal(err.message, message); setupClient.destroy(); t.end(); }); }); tap.test('abort reconnect', function (t) { const abortClient = ldap.createClient({ connectTimeout: parseInt(LDAP_CONNECT_TIMEOUT, 10), socketPath: 'an invalid path', reconnect: true }); var retryCount = 0; abortClient.on('connectError', function () { ++retryCount; }); abortClient.once('connectError', function () { t.ok(true); abortClient.once('destroy', function () { t.ok(retryCount < 3); t.end(); }); abortClient.destroy(); }); }); tap.test('reconnect max retries', function (t) { const RETRIES = 5; const rClient = ldap.createClient({ connectTimeout: 100, socketPath: 'an invalid path', reconnect: { failAfter: RETRIES, // Keep the test duration low initialDelay: 10, maxDelay: 100 } }); let count = 0; rClient.on('connectError', function () { count++; }); rClient.on('error', function (err) { t.equal(count, RETRIES); rClient.destroy(); t.end(); }); }); tap.test('reconnect on server close', function (t) { const clt = ldap.createClient({ socketPath: t.context.socketPath, reconnect: true }); clt.on('setup', function (sclt, cb) { sclt.bind(BIND_DN, BIND_PW, function (err, res) { t.error(err); cb(err); }); }); clt.once('connect', function () { t.ok(clt._socket); clt.once('connect', function () { t.ok(true, 'successful reconnect'); clt.destroy(); t.end(); }); // Simulate server-side close clt._socket.destroy(); }); }); tap.test('no auto-reconnect on unbind', function (t) { const clt = ldap.createClient({ socketPath: t.context.socketPath, reconnect: true }); clt.on('setup', function (sclt, cb) { sclt.bind(BIND_DN, BIND_PW, function (err, res) { t.error(err); cb(err); }); }); clt.once('connect', function () { clt.once('connect', function () { t.error(new Error('client should not reconnect')); }); clt.once('close', function () { t.ok(true, 'initial close'); setImmediate(function () { t.ok(!clt.connected, 'should not be connected'); t.ok(!clt.connecting, 'should not be connecting'); clt.destroy(); t.end(); }); }); clt.unbind(); }); }); tap.test('abandon (GH-27)', function (t) { // FIXME: test abandoning a real request t.context.client.abandon(401876543, function (err) { t.error(err); t.end(); }); }); tap.test('search timeout (GH-51)', function (t) { t.context.client.timeout = 250; t.context.client.search('dc=timeout', 'objectclass=*', function (err, res) { t.error(err); res.on('error', function () { t.end(); }); }); }); tap.test('resultError handling', function (t) { const client = t.context.client; vasync.pipeline({ funcs: [errSearch, cleanSearch] }, function (err) { t.error(err); client.removeListener('resultError', error1); client.removeListener('resultError', error2); t.end(); }); function errSearch(_, cb) { client.once('resultError', error1); client.search('cn=busy', {}, function (err, res) { res.once('error', function (error) { t.equal(error.name, 'BusyError'); cb(); }); }); } function cleanSearch(_, cb) { client.on('resultError', error2); client.search(SUFFIX, {}, function (err, res) { res.once('end', function () { t.pass(); cb(); }); }); } function error1 (error) { t.equal(error.name, 'BusyError'); } function error2 () { t.fail('should not get error') } });