@ldapjs/messages
Version:
API for creating and parsing LDAP messages
439 lines (370 loc) • 10.5 kB
JavaScript
'use strict'
const tap = require('tap')
const { operations } = require('@ldapjs/protocol')
const filter = require('@ldapjs/filter')
const SearchRequest = require('./search-request')
const { DN } = require('@ldapjs/dn')
const { BerReader, BerWriter } = require('@ldapjs/asn1')
const warning = require('../deprecations')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
const {
searchRequestBytes
} = require('./_fixtures/message-byte-arrays')
tap.test('basic', t => {
t.test('constructor no args', async t => {
const req = new SearchRequest()
const pojo = req.pojo
t.strictSame(pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_SEARCH,
type: 'SearchRequest',
baseObject: '',
scope: 'base',
derefAliases: SearchRequest.DEREF_ALIASES_NEVER,
sizeLimit: 0,
timeLimit: 0,
typesOnly: false,
filter: '(objectclass=*)',
attributes: [],
controls: []
})
t.equal(req.type, 'SearchRequest')
})
t.test('constructor with args', async t => {
const req = new SearchRequest({
baseObject: 'cn=foo,dc=example,dc=com',
scope: SearchRequest.SCOPE_SUBTREE,
derefAliases: SearchRequest.DEREF_BASE_OBJECT,
sizeLimit: 1,
timeLimit: 1,
typesOnly: true,
filter: new filter.EqualityFilter({ attribute: 'cn', value: 'foo' }),
attributes: ['*']
})
const pojo = req.pojo
t.strictSame(pojo, {
messageId: 1,
protocolOp: operations.LDAP_REQ_SEARCH,
type: 'SearchRequest',
baseObject: 'cn=foo,dc=example,dc=com',
scope: 'subtree',
derefAliases: SearchRequest.DEREF_BASE_OBJECT,
sizeLimit: 1,
timeLimit: 1,
typesOnly: true,
filter: '(cn=foo)',
attributes: ['*'],
controls: []
})
})
t.end()
})
tap.test('.attributes', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.strictSame(req.attributes, [])
req.attributes = ['*']
t.strictSame(req.attributes, ['*'])
})
t.test('set overwrites current list', async t => {
const req = new SearchRequest({
attributes: ['1.1']
})
req.attributes = ['1.1', '*', '@foo3-bar.2', 'cn', 'sn;lang-en']
t.strictSame(req.attributes, ['1.1', '*', '@foo3-bar.2', 'cn', 'sn;lang-en'])
})
t.test('throws if not an array', async t => {
t.throws(
() => new SearchRequest({ attributes: '*' }),
'attributes must be an array of attribute strings'
)
})
t.test('throws if array contains non-attribute', async t => {
const input = [
'*',
'not allowed'
]
t.throws(
() => new SearchRequest({ attributes: input }),
'attribute must be a valid string'
)
})
t.test('supports single character names (issue #2)', async t => {
const req = new SearchRequest({
attributes: ['a']
})
t.strictSame(req.attributes, ['a'])
})
t.test('supports multiple attribute options', async t => {
const req = new SearchRequest({
attributes: ['abc;lang-en;lang-es']
})
t.strictSame(req.attributes, ['abc;lang-en;lang-es'])
})
t.test('supports range options', async t => {
const req = new SearchRequest({
attributes: [
'a;range=0-*',
'abc;range=100-200',
'def;range=0-5;lang-en',
'ghi;lang-en;range=6-10',
'jkl;lang-en;range=11-15;lang-es'
]
})
t.strictSame(req.attributes, [
'a;range=0-*',
'abc;range=100-200',
'def;range=0-5;lang-en',
'ghi;lang-en;range=6-10',
'jkl;lang-en;range=11-15;lang-es'
])
})
t.test('throws if array contains an invalid range', async t => {
const input = ['a;range=*-100']
t.throws(
() => new SearchRequest({ attributes: input }),
'attribute must be a valid string'
)
})
t.test('skip empty attribute name', async t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_ATTRIBUTE_SPEC_ERR_001', false)
})
const req = new SearchRequest({
attributes: ['abc', '']
})
t.strictSame(req.attributes, ['abc'])
function handler (error) {
t.equal(error.message, 'received attempt to define attribute with an empty name: attribute skipped.')
t.end()
}
})
t.end()
})
tap.test('.baseObject', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.baseObject.toString(), '')
req.baseObject = 'dc=example,dc=com'
t.equal(req.baseObject.toString(), 'dc=example,dc=com')
req.baseObject = DN.fromString('dc=example,dc=net')
t.equal(req.baseObject.toString(), 'dc=example,dc=net')
t.equal(req._dn.toString(), 'dc=example,dc=net')
})
t.test('throws for non-DN object', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.baseObject = ['foo']
},
'baseObject must be a DN string or DN instance'
)
})
t.end()
})
tap.test('.derefAliases', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.derefAliases, SearchRequest.DEREF_ALIASES_NEVER)
req.derefAliases = SearchRequest.DEREF_ALWAYS
t.equal(req.derefAliases, SearchRequest.DEREF_ALWAYS)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.derefAliases = '0'
},
'derefAliases must be set to an integer'
)
})
t.end()
})
tap.test('.filter', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.filter.toString(), '(objectclass=*)')
req.filter = '(cn=foo)'
t.equal(req.filter.toString(), '(cn=foo)')
req.filter = new filter.EqualityFilter({ attribute: 'sn', value: 'bar' })
t.equal(req.filter.toString(), '(sn=bar)')
})
t.test('throws for bad value', async t => {
const expected = 'filter must be a string or a FilterString instance'
const req = new SearchRequest()
t.throws(
() => {
req.filter = ['foo']
},
expected
)
t.throws(
() => {
req.filter = { attribute: 'cn', value: 'foo' }
},
expected
)
})
t.end()
})
tap.test('.scope', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.scopeName, 'base')
t.equal(req.scope, 0)
req.scope = SearchRequest.SCOPE_SINGLE
t.equal(req.scopeName, 'single')
t.equal(req.scope, 1)
req.scope = SearchRequest.SCOPE_SUBTREE
t.equal(req.scopeName, 'subtree')
t.equal(req.scope, 2)
req.scope = 'SUB'
t.equal(req.scopeName, 'subtree')
t.equal(req.scope, 2)
req.scope = 'base'
t.equal(req.scopeName, 'base')
t.equal(req.scope, 0)
})
t.test('throws for invalid value', async t => {
const expected = ' is an invalid search scope'
const req = new SearchRequest()
t.throws(
() => {
req.scope = 'nested'
},
'nested' + expected
)
t.throws(
() => {
req.scope = 42
},
42 + expected
)
})
t.end()
})
tap.test('.sizeLimit', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.sizeLimit, 0)
req.sizeLimit = 15
t.equal(req.sizeLimit, 15)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.sizeLimit = 15.5
},
'sizeLimit must be an integer'
)
})
t.end()
})
tap.test('.timeLimit', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.timeLimit, 0)
req.timeLimit = 15
t.equal(req.timeLimit, 15)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.timeLimit = 15.5
},
'timeLimit must be an integer'
)
})
t.end()
})
tap.test('.typesOnly', t => {
t.test('sets/gets', async t => {
const req = new SearchRequest()
t.equal(req.typesOnly, false)
req.typesOnly = true
t.equal(req.typesOnly, true)
})
t.test('throws for bad value', async t => {
const req = new SearchRequest()
t.throws(
() => {
req.typesOnly = 'true'
},
'typesOnly must be set to a boolean value'
)
})
t.end()
})
tap.test('_toBer', t => {
tap.test('converts instance to BER', async t => {
const req = new SearchRequest({
messageId: 2,
baseObject: 'dc=example,dc=com',
scope: 'subtree',
derefAliases: SearchRequest.DEREF_ALIASES_NEVER,
sizeLimit: 1000,
timeLimit: 30,
typesOnly: false,
filter: '(&(objectClass=person)(uid=jdoe))',
attributes: ['*', '+']
})
const writer = new BerWriter()
req._toBer(writer)
t.equal(
Buffer.from(searchRequestBytes.slice(5)).compare(writer.buffer),
0
)
})
t.end()
})
tap.test('_pojo', t => {
t.test('returns a pojo representation', async t => {
const req = new SearchRequest()
t.strictSame(req._pojo(), {
baseObject: '',
scope: 'base',
derefAliases: 0,
sizeLimit: 0,
timeLimit: 0,
typesOnly: false,
filter: '(objectclass=*)',
attributes: []
})
})
t.end()
})
tap.test('#parseToPojo', t => {
t.test('throws if operation incorrect', async t => {
const reqBuffer = Buffer.from(searchRequestBytes)
reqBuffer[5] = 0x61
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
t.throws(
() => SearchRequest.parseToPojo(reader),
Error('found wrong protocol operation: 0x61')
)
})
t.test('returns a pojo representation', async t => {
const reqBuffer = Buffer.from(searchRequestBytes)
const reader = new BerReader(reqBuffer)
reader.readSequence()
reader.readInt()
const pojo = SearchRequest.parseToPojo(reader)
t.equal(pojo.protocolOp, operations.LDAP_REQ_SEARCH)
t.equal(pojo.baseObject, 'dc=example,dc=com')
t.equal(pojo.scope, SearchRequest.SCOPE_SUBTREE)
t.equal(pojo.derefAliases, SearchRequest.DEREF_ALIASES_NEVER)
t.equal(pojo.sizeLimit, 1000)
t.equal(pojo.timeLimit, 30)
t.equal(pojo.typesOnly, false)
t.equal(pojo.filter, '(&(objectClass=person)(uid=jdoe))')
t.strictSame(pojo.attributes, ['*', '+'])
})
t.end()
})