search-request.test.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. 'use strict'
  2. const tap = require('tap')
  3. const { operations } = require('@ldapjs/protocol')
  4. const filter = require('@ldapjs/filter')
  5. const SearchRequest = require('./search-request')
  6. const { DN } = require('@ldapjs/dn')
  7. const { BerReader, BerWriter } = require('@ldapjs/asn1')
  8. const warning = require('../deprecations')
  9. // Silence the standard warning logs. We will test the messages explicitly.
  10. process.removeAllListeners('warning')
  11. const {
  12. searchRequestBytes
  13. } = require('./_fixtures/message-byte-arrays')
  14. tap.test('basic', t => {
  15. t.test('constructor no args', async t => {
  16. const req = new SearchRequest()
  17. const pojo = req.pojo
  18. t.strictSame(pojo, {
  19. messageId: 1,
  20. protocolOp: operations.LDAP_REQ_SEARCH,
  21. type: 'SearchRequest',
  22. baseObject: '',
  23. scope: 'base',
  24. derefAliases: SearchRequest.DEREF_ALIASES_NEVER,
  25. sizeLimit: 0,
  26. timeLimit: 0,
  27. typesOnly: false,
  28. filter: '(objectclass=*)',
  29. attributes: [],
  30. controls: []
  31. })
  32. t.equal(req.type, 'SearchRequest')
  33. })
  34. t.test('constructor with args', async t => {
  35. const req = new SearchRequest({
  36. baseObject: 'cn=foo,dc=example,dc=com',
  37. scope: SearchRequest.SCOPE_SUBTREE,
  38. derefAliases: SearchRequest.DEREF_BASE_OBJECT,
  39. sizeLimit: 1,
  40. timeLimit: 1,
  41. typesOnly: true,
  42. filter: new filter.EqualityFilter({ attribute: 'cn', value: 'foo' }),
  43. attributes: ['*']
  44. })
  45. const pojo = req.pojo
  46. t.strictSame(pojo, {
  47. messageId: 1,
  48. protocolOp: operations.LDAP_REQ_SEARCH,
  49. type: 'SearchRequest',
  50. baseObject: 'cn=foo,dc=example,dc=com',
  51. scope: 'subtree',
  52. derefAliases: SearchRequest.DEREF_BASE_OBJECT,
  53. sizeLimit: 1,
  54. timeLimit: 1,
  55. typesOnly: true,
  56. filter: '(cn=foo)',
  57. attributes: ['*'],
  58. controls: []
  59. })
  60. })
  61. t.end()
  62. })
  63. tap.test('.attributes', t => {
  64. t.test('sets/gets', async t => {
  65. const req = new SearchRequest()
  66. t.strictSame(req.attributes, [])
  67. req.attributes = ['*']
  68. t.strictSame(req.attributes, ['*'])
  69. })
  70. t.test('set overwrites current list', async t => {
  71. const req = new SearchRequest({
  72. attributes: ['1.1']
  73. })
  74. req.attributes = ['1.1', '*', '@foo3-bar.2', 'cn', 'sn;lang-en']
  75. t.strictSame(req.attributes, ['1.1', '*', '@foo3-bar.2', 'cn', 'sn;lang-en'])
  76. })
  77. t.test('throws if not an array', async t => {
  78. t.throws(
  79. () => new SearchRequest({ attributes: '*' }),
  80. 'attributes must be an array of attribute strings'
  81. )
  82. })
  83. t.test('throws if array contains non-attribute', async t => {
  84. const input = [
  85. '*',
  86. 'not allowed'
  87. ]
  88. t.throws(
  89. () => new SearchRequest({ attributes: input }),
  90. 'attribute must be a valid string'
  91. )
  92. })
  93. t.test('supports single character names (issue #2)', async t => {
  94. const req = new SearchRequest({
  95. attributes: ['a']
  96. })
  97. t.strictSame(req.attributes, ['a'])
  98. })
  99. t.test('supports multiple attribute options', async t => {
  100. const req = new SearchRequest({
  101. attributes: ['abc;lang-en;lang-es']
  102. })
  103. t.strictSame(req.attributes, ['abc;lang-en;lang-es'])
  104. })
  105. t.test('supports range options', async t => {
  106. const req = new SearchRequest({
  107. attributes: [
  108. 'a;range=0-*',
  109. 'abc;range=100-200',
  110. 'def;range=0-5;lang-en',
  111. 'ghi;lang-en;range=6-10',
  112. 'jkl;lang-en;range=11-15;lang-es'
  113. ]
  114. })
  115. t.strictSame(req.attributes, [
  116. 'a;range=0-*',
  117. 'abc;range=100-200',
  118. 'def;range=0-5;lang-en',
  119. 'ghi;lang-en;range=6-10',
  120. 'jkl;lang-en;range=11-15;lang-es'
  121. ])
  122. })
  123. t.test('throws if array contains an invalid range', async t => {
  124. const input = ['a;range=*-100']
  125. t.throws(
  126. () => new SearchRequest({ attributes: input }),
  127. 'attribute must be a valid string'
  128. )
  129. })
  130. t.test('skip empty attribute name', async t => {
  131. process.on('warning', handler)
  132. t.teardown(async () => {
  133. process.removeListener('warning', handler)
  134. warning.emitted.set('LDAP_ATTRIBUTE_SPEC_ERR_001', false)
  135. })
  136. const req = new SearchRequest({
  137. attributes: ['abc', '']
  138. })
  139. t.strictSame(req.attributes, ['abc'])
  140. function handler (error) {
  141. t.equal(error.message, 'received attempt to define attribute with an empty name: attribute skipped.')
  142. t.end()
  143. }
  144. })
  145. t.end()
  146. })
  147. tap.test('.baseObject', t => {
  148. t.test('sets/gets', async t => {
  149. const req = new SearchRequest()
  150. t.equal(req.baseObject.toString(), '')
  151. req.baseObject = 'dc=example,dc=com'
  152. t.equal(req.baseObject.toString(), 'dc=example,dc=com')
  153. req.baseObject = DN.fromString('dc=example,dc=net')
  154. t.equal(req.baseObject.toString(), 'dc=example,dc=net')
  155. t.equal(req._dn.toString(), 'dc=example,dc=net')
  156. })
  157. t.test('throws for non-DN object', async t => {
  158. const req = new SearchRequest()
  159. t.throws(
  160. () => {
  161. req.baseObject = ['foo']
  162. },
  163. 'baseObject must be a DN string or DN instance'
  164. )
  165. })
  166. t.end()
  167. })
  168. tap.test('.derefAliases', t => {
  169. t.test('sets/gets', async t => {
  170. const req = new SearchRequest()
  171. t.equal(req.derefAliases, SearchRequest.DEREF_ALIASES_NEVER)
  172. req.derefAliases = SearchRequest.DEREF_ALWAYS
  173. t.equal(req.derefAliases, SearchRequest.DEREF_ALWAYS)
  174. })
  175. t.test('throws for bad value', async t => {
  176. const req = new SearchRequest()
  177. t.throws(
  178. () => {
  179. req.derefAliases = '0'
  180. },
  181. 'derefAliases must be set to an integer'
  182. )
  183. })
  184. t.end()
  185. })
  186. tap.test('.filter', t => {
  187. t.test('sets/gets', async t => {
  188. const req = new SearchRequest()
  189. t.equal(req.filter.toString(), '(objectclass=*)')
  190. req.filter = '(cn=foo)'
  191. t.equal(req.filter.toString(), '(cn=foo)')
  192. req.filter = new filter.EqualityFilter({ attribute: 'sn', value: 'bar' })
  193. t.equal(req.filter.toString(), '(sn=bar)')
  194. })
  195. t.test('throws for bad value', async t => {
  196. const expected = 'filter must be a string or a FilterString instance'
  197. const req = new SearchRequest()
  198. t.throws(
  199. () => {
  200. req.filter = ['foo']
  201. },
  202. expected
  203. )
  204. t.throws(
  205. () => {
  206. req.filter = { attribute: 'cn', value: 'foo' }
  207. },
  208. expected
  209. )
  210. })
  211. t.end()
  212. })
  213. tap.test('.scope', t => {
  214. t.test('sets/gets', async t => {
  215. const req = new SearchRequest()
  216. t.equal(req.scopeName, 'base')
  217. t.equal(req.scope, 0)
  218. req.scope = SearchRequest.SCOPE_SINGLE
  219. t.equal(req.scopeName, 'single')
  220. t.equal(req.scope, 1)
  221. req.scope = SearchRequest.SCOPE_SUBTREE
  222. t.equal(req.scopeName, 'subtree')
  223. t.equal(req.scope, 2)
  224. req.scope = 'SUB'
  225. t.equal(req.scopeName, 'subtree')
  226. t.equal(req.scope, 2)
  227. req.scope = 'base'
  228. t.equal(req.scopeName, 'base')
  229. t.equal(req.scope, 0)
  230. })
  231. t.test('throws for invalid value', async t => {
  232. const expected = ' is an invalid search scope'
  233. const req = new SearchRequest()
  234. t.throws(
  235. () => {
  236. req.scope = 'nested'
  237. },
  238. 'nested' + expected
  239. )
  240. t.throws(
  241. () => {
  242. req.scope = 42
  243. },
  244. 42 + expected
  245. )
  246. })
  247. t.end()
  248. })
  249. tap.test('.sizeLimit', t => {
  250. t.test('sets/gets', async t => {
  251. const req = new SearchRequest()
  252. t.equal(req.sizeLimit, 0)
  253. req.sizeLimit = 15
  254. t.equal(req.sizeLimit, 15)
  255. })
  256. t.test('throws for bad value', async t => {
  257. const req = new SearchRequest()
  258. t.throws(
  259. () => {
  260. req.sizeLimit = 15.5
  261. },
  262. 'sizeLimit must be an integer'
  263. )
  264. })
  265. t.end()
  266. })
  267. tap.test('.timeLimit', t => {
  268. t.test('sets/gets', async t => {
  269. const req = new SearchRequest()
  270. t.equal(req.timeLimit, 0)
  271. req.timeLimit = 15
  272. t.equal(req.timeLimit, 15)
  273. })
  274. t.test('throws for bad value', async t => {
  275. const req = new SearchRequest()
  276. t.throws(
  277. () => {
  278. req.timeLimit = 15.5
  279. },
  280. 'timeLimit must be an integer'
  281. )
  282. })
  283. t.end()
  284. })
  285. tap.test('.typesOnly', t => {
  286. t.test('sets/gets', async t => {
  287. const req = new SearchRequest()
  288. t.equal(req.typesOnly, false)
  289. req.typesOnly = true
  290. t.equal(req.typesOnly, true)
  291. })
  292. t.test('throws for bad value', async t => {
  293. const req = new SearchRequest()
  294. t.throws(
  295. () => {
  296. req.typesOnly = 'true'
  297. },
  298. 'typesOnly must be set to a boolean value'
  299. )
  300. })
  301. t.end()
  302. })
  303. tap.test('_toBer', t => {
  304. tap.test('converts instance to BER', async t => {
  305. const req = new SearchRequest({
  306. messageId: 2,
  307. baseObject: 'dc=example,dc=com',
  308. scope: 'subtree',
  309. derefAliases: SearchRequest.DEREF_ALIASES_NEVER,
  310. sizeLimit: 1000,
  311. timeLimit: 30,
  312. typesOnly: false,
  313. filter: '(&(objectClass=person)(uid=jdoe))',
  314. attributes: ['*', '+']
  315. })
  316. const writer = new BerWriter()
  317. req._toBer(writer)
  318. t.equal(
  319. Buffer.from(searchRequestBytes.slice(5)).compare(writer.buffer),
  320. 0
  321. )
  322. })
  323. t.end()
  324. })
  325. tap.test('_pojo', t => {
  326. t.test('returns a pojo representation', async t => {
  327. const req = new SearchRequest()
  328. t.strictSame(req._pojo(), {
  329. baseObject: '',
  330. scope: 'base',
  331. derefAliases: 0,
  332. sizeLimit: 0,
  333. timeLimit: 0,
  334. typesOnly: false,
  335. filter: '(objectclass=*)',
  336. attributes: []
  337. })
  338. })
  339. t.end()
  340. })
  341. tap.test('#parseToPojo', t => {
  342. t.test('throws if operation incorrect', async t => {
  343. const reqBuffer = Buffer.from(searchRequestBytes)
  344. reqBuffer[5] = 0x61
  345. const reader = new BerReader(reqBuffer)
  346. reader.readSequence()
  347. reader.readInt()
  348. t.throws(
  349. () => SearchRequest.parseToPojo(reader),
  350. Error('found wrong protocol operation: 0x61')
  351. )
  352. })
  353. t.test('returns a pojo representation', async t => {
  354. const reqBuffer = Buffer.from(searchRequestBytes)
  355. const reader = new BerReader(reqBuffer)
  356. reader.readSequence()
  357. reader.readInt()
  358. const pojo = SearchRequest.parseToPojo(reader)
  359. t.equal(pojo.protocolOp, operations.LDAP_REQ_SEARCH)
  360. t.equal(pojo.baseObject, 'dc=example,dc=com')
  361. t.equal(pojo.scope, SearchRequest.SCOPE_SUBTREE)
  362. t.equal(pojo.derefAliases, SearchRequest.DEREF_ALIASES_NEVER)
  363. t.equal(pojo.sizeLimit, 1000)
  364. t.equal(pojo.timeLimit, 30)
  365. t.equal(pojo.typesOnly, false)
  366. t.equal(pojo.filter, '(&(objectClass=person)(uid=jdoe))')
  367. t.strictSame(pojo.attributes, ['*', '+'])
  368. })
  369. t.end()
  370. })