comq 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/docs/headers.md +16 -1
  2. package/features/properties.feature +39 -0
  3. package/features/shards.virtual.feature +89 -0
  4. package/features/steps/context.js +29 -2
  5. package/features/steps/hooks.js +1 -0
  6. package/features/steps/names.js +245 -0
  7. package/features/steps/networks.js +23 -5
  8. package/features/steps/rpc.js +14 -2
  9. package/package.json +1 -1
  10. package/readme.md +102 -4
  11. package/source/channel.js +147 -2
  12. package/source/connect.js +18 -15
  13. package/source/connection.js +8 -5
  14. package/source/events.js +7 -0
  15. package/source/io.js +31 -9
  16. package/source/shards/channel.js +289 -19
  17. package/source/shards/connection.js +404 -18
  18. package/source/shards/expand.js +57 -0
  19. package/source/shards/index.js +2 -0
  20. package/source/shards/resolve.js +134 -0
  21. package/source/shards/watch.js +257 -0
  22. package/source/singleton.js +3 -2
  23. package/test/amqplib.mock.js +1 -0
  24. package/test/assert.test.js +7 -2
  25. package/test/channel.quiet.test.js +301 -0
  26. package/test/connect.test.js +49 -8
  27. package/test/connection.mock.js +3 -0
  28. package/test/connection.test.js +15 -0
  29. package/test/io.call.test.js +1 -1
  30. package/test/io.diagnostics.test.js +1 -1
  31. package/test/io.reply.test.js +12 -1
  32. package/test/io.request.test.js +18 -0
  33. package/test/io.shards.test.js +85 -0
  34. package/test/shards/channel.test.js +411 -0
  35. package/test/shards/connection.test.js +372 -17
  36. package/test/shards/expand.test.js +58 -0
  37. package/test/shards/resolve.test.js +73 -0
  38. package/test/shards/watch.test.js +264 -0
  39. package/test/singleton.test.js +1 -1
  40. package/types/channel.d.ts +23 -0
  41. package/types/connection.d.ts +13 -2
  42. package/types/diagnostic.d.ts +2 -1
  43. package/types/io.d.ts +17 -1
  44. package/types/shards.d.ts +58 -0
@@ -15,32 +15,79 @@ it('should be', async () => {
15
15
  expect(Connection).toBeDefined()
16
16
  })
17
17
 
18
- /** @type {jest.MockedObject<comq.Connection>[]} */
18
+ /** @type {jest.MockedObject<comq.Connection>[]} the connections made, in the order they were */
19
19
  let connections
20
20
 
21
+ /** @type {jest.MockedFunction<(url: string, servername?: string) => comq.Connection>} */
22
+ let make
23
+
21
24
  /** @type {comq.Connection} */
22
25
  let connection
23
26
 
27
+ /** @type {jest.MockedObject<comq.Channel>[]} */
28
+ let channels
29
+
30
+ const urls = ['amqp://10.0.0.1', 'amqp://10.0.0.2']
31
+
32
+ const sharded = () => {
33
+ const channel = {
34
+ has: jest.fn(() => false),
35
+ join: jest.fn(async () => undefined),
36
+ retire: jest.fn(() => undefined),
37
+ restore: jest.fn(() => undefined),
38
+ quiet: jest.fn(async () => 0),
39
+ leave: jest.fn(async () => undefined)
40
+ }
41
+
42
+ channels.push(channel)
43
+
44
+ return channel
45
+ }
46
+
24
47
  beforeEach(() => {
25
48
  jest.clearAllMocks()
26
49
 
27
- connections = [mock.connection(), mock.connection()]
28
- connection = new Connection(connections)
50
+ connections = []
51
+ channels = []
52
+
53
+ make = jest.fn(() => {
54
+ const connection = mock.connection()
55
+
56
+ connections.push(connection)
57
+
58
+ return connection
59
+ })
60
+
61
+ create.mockImplementation(async () => sharded())
62
+
63
+ connection = new Connection(urls, make)
29
64
  })
30
65
 
31
66
  describe('open', () => {
67
+ it('should make a connection to each of the urls', async () => {
68
+ await connection.open()
69
+
70
+ expect(make).toHaveBeenCalledTimes(2)
71
+ urls.forEach((url) => expect(make).toHaveBeenCalledWith(url, undefined))
72
+ connections.forEach((conn) => expect(conn.open).toHaveBeenCalled())
73
+ })
74
+
32
75
  it('should resolve when all of the connections are established', async () => {
33
76
  expect.assertions(2)
34
77
 
35
78
  /** @type {Promex[]} */
36
79
  const promises = []
37
80
 
38
- for (const conn of connections) {
81
+ make.mockImplementation(() => {
82
+ const conn = mock.connection()
39
83
  const promise = new Promex()
40
84
 
41
85
  conn.open.mockImplementation(() => promise)
42
86
  promises.push(promise)
43
- }
87
+ connections.push(conn)
88
+
89
+ return conn
90
+ })
44
91
 
45
92
  let resolved = false
46
93
 
@@ -64,21 +111,29 @@ describe('open', () => {
64
111
  })
65
112
 
66
113
  it('should close opened connection if one fails', async () => {
67
- connections.push(mock.connection())
68
-
69
- const [one, bad, two] = connections
70
114
  const exception = new Error(generate())
71
115
 
72
- one.open.mockImplementation(() => Promise.resolve())
73
- two.open.mockImplementation(() => Promise.resolve())
116
+ connection = new Connection([...urls, 'amqp://10.0.0.3'], make)
74
117
 
75
- bad.open.mockImplementation(async () => {
76
- await immediate()
77
- throw exception
118
+ make.mockImplementation(() => {
119
+ const conn = mock.connection()
120
+
121
+ if (connections.length === 1) {
122
+ conn.open.mockImplementation(async () => {
123
+ await immediate()
124
+ throw exception
125
+ })
126
+ }
127
+
128
+ connections.push(conn)
129
+
130
+ return conn
78
131
  })
79
132
 
80
133
  await expect(connection.open()).rejects.toThrow(exception)
81
134
 
135
+ const [one, , two] = connections
136
+
82
137
  expect(one.close).toHaveBeenCalled()
83
138
  expect(two.close).toHaveBeenCalled()
84
139
  })
@@ -86,6 +141,7 @@ describe('open', () => {
86
141
 
87
142
  describe('close', () => {
88
143
  it('should close all connections', async () => {
144
+ await connection.open()
89
145
  await connection.close()
90
146
 
91
147
  for (const conn of connections) {
@@ -97,17 +153,20 @@ describe('close', () => {
97
153
  describe('createChannel', () => {
98
154
  const type = generate()
99
155
 
100
- beforeEach(() => {
101
- connection.createChannel(type)
156
+ beforeEach(async () => {
157
+ await connection.open()
158
+ await connection.createChannel(type)
102
159
  })
103
160
 
104
- it('should create channel', async () => {
105
- expect(create).toHaveBeenCalledWith(connections, type)
161
+ it('should create channel over the connections, by the number of each', async () => {
162
+ expect(create).toHaveBeenCalledWith(new Map(connections.entries()), type)
106
163
  })
107
164
  })
108
165
 
109
166
  describe.each(/** @type {comq.diagnostics.Event[]} */ ['open', 'close'])('diagnose %s event',
110
167
  (event) => {
168
+ beforeEach(() => connection.open())
169
+
111
170
  it('should re-emit event', async () => {
112
171
  const index = random(connections.length)
113
172
 
@@ -134,6 +193,8 @@ describe.each(/** @type {comq.diagnostics.Event[]} */ ['open', 'close'])('diagno
134
193
 
135
194
  describe('forget', () => {
136
195
  it('should stop re-emitting to the listener', async () => {
196
+ await connection.open()
197
+
137
198
  const listener = /** @type {Function} */ jest.fn()
138
199
 
139
200
  connection.diagnose('open', listener)
@@ -146,3 +207,297 @@ describe('forget', () => {
146
207
  expect(listener).not.toHaveBeenCalled()
147
208
  })
148
209
  })
210
+
211
+ describe('names', () => {
212
+ const INTERVAL = 1000
213
+ const SETTLE = 3000
214
+ const LINGER = 5000
215
+
216
+ const names = ['amqp://one', 'amqp://two', 'amqp://three', 'amqp://four']
217
+
218
+ /** @type {Record<string, string>} */
219
+ let addresses
220
+
221
+ /** @type {Record<string, jest.MockedFunction<Function>>} */
222
+ let heard
223
+
224
+ const tick = (ms = INTERVAL) => jest.advanceTimersByTimeAsync(ms)
225
+
226
+ /**
227
+ * @param {jest.MockedObject<comq.Connection>} conn
228
+ * @param {comq.diagnostics.Event} event
229
+ * @param {...any} args
230
+ */
231
+ const tell = (conn, event, ...args) => {
232
+ for (const [name, listener] of conn.diagnose.mock.calls) if (name === event) listener(...args)
233
+ }
234
+
235
+ beforeEach(async () => {
236
+ jest.useFakeTimers()
237
+
238
+ addresses = { one: '10.0.0.1', two: '10.0.0.1', three: '10.0.0.2', four: '10.0.0.2' }
239
+
240
+ global.COMQ_TESTING_LOOKUP = async (host) => ({ addresses: [addresses[host]], ttl: 1 })
241
+
242
+ connection = new Connection(names, make, { interval: INTERVAL, settle: SETTLE, linger: LINGER })
243
+ heard = { join: jest.fn(), retire: jest.fn(), leave: jest.fn() }
244
+
245
+ for (const [event, listener] of Object.entries(heard)) connection.diagnose(event, listener)
246
+
247
+ await connection.open()
248
+ await connection.createChannel(generate())
249
+ })
250
+
251
+ afterEach(async () => {
252
+ await connection.close()
253
+
254
+ jest.useRealTimers()
255
+
256
+ delete global.COMQ_TESTING_LOOKUP
257
+ })
258
+
259
+ it('should make a connection to each broker rather than to each name', async () => {
260
+ expect(make).toHaveBeenCalledTimes(2)
261
+ expect(make).toHaveBeenCalledWith('amqp://10.0.0.1', 'one')
262
+ expect(make).toHaveBeenCalledWith('amqp://10.0.0.2', 'three')
263
+ })
264
+
265
+ it('should join a broker a name has moved to, once that has settled', async () => {
266
+ addresses.four = '10.0.0.3'
267
+
268
+ await tick(SETTLE)
269
+
270
+ expect(make).toHaveBeenCalledTimes(2)
271
+
272
+ await tick()
273
+
274
+ expect(make).toHaveBeenCalledWith('amqp://10.0.0.3', 'four')
275
+ expect(connections[2].open).toHaveBeenCalled()
276
+ expect(channels[0].join).toHaveBeenCalledWith(connections[2], 2)
277
+ expect(heard.join).toHaveBeenCalledWith(2, '10.0.0.3')
278
+ })
279
+
280
+ it('should not retire a broker that is still named', async () => {
281
+ addresses.four = '10.0.0.3'
282
+
283
+ await tick(SETTLE + LINGER * 2)
284
+
285
+ expect(channels[0].retire).not.toHaveBeenCalled()
286
+ expect(heard.retire).not.toHaveBeenCalled()
287
+ })
288
+
289
+ it('should retire a broker no name points at', async () => {
290
+ addresses.three = '10.0.0.3'
291
+ addresses.four = '10.0.0.3'
292
+
293
+ await tick(SETTLE + INTERVAL)
294
+
295
+ expect(channels[0].retire).toHaveBeenCalledWith(1)
296
+ expect(heard.retire).toHaveBeenCalledWith(1, '10.0.0.2')
297
+ expect(connections[1].close).not.toHaveBeenCalled()
298
+ })
299
+
300
+ it('should not retire a broker before the one named instead has joined', async () => {
301
+ const opening = new Promex()
302
+
303
+ make.mockImplementation(() => {
304
+ const conn = mock.connection()
305
+
306
+ conn.open.mockImplementation(() => opening)
307
+ connections.push(conn)
308
+
309
+ return conn
310
+ })
311
+
312
+ addresses.three = '10.0.0.3'
313
+ addresses.four = '10.0.0.3'
314
+
315
+ await tick(SETTLE + LINGER * 2)
316
+
317
+ expect(heard.retire).not.toHaveBeenCalled()
318
+
319
+ opening.resolve()
320
+
321
+ await tick(0)
322
+
323
+ expect(heard.retire).toHaveBeenCalledWith(1, '10.0.0.2')
324
+ })
325
+
326
+ it('should take a shard that has joined into a channel made later', async () => {
327
+ addresses.four = '10.0.0.3'
328
+
329
+ await tick(SETTLE + INTERVAL)
330
+ await connection.createChannel(generate())
331
+
332
+ expect(create).toHaveBeenLastCalledWith(new Map(connections.entries()), expect.any(String))
333
+ })
334
+
335
+ describe('retired', () => {
336
+ beforeEach(async () => {
337
+ addresses.three = '10.0.0.3'
338
+ addresses.four = '10.0.0.3'
339
+
340
+ await tick(SETTLE + INTERVAL)
341
+ })
342
+
343
+ it('should leave a broker that has been idle for the linger time', async () => {
344
+ await tick(LINGER - INTERVAL)
345
+
346
+ expect(heard.leave).not.toHaveBeenCalled()
347
+
348
+ await tick(INTERVAL * 2)
349
+
350
+ expect(channels[0].leave).toHaveBeenCalledWith(1)
351
+ expect(connections[1].close).toHaveBeenCalled()
352
+ expect(heard.leave).toHaveBeenCalledWith(1, '10.0.0.2')
353
+ })
354
+
355
+ it('should not leave a broker that has something to do', async () => {
356
+ channels[0].quiet.mockImplementation(async () => Date.now())
357
+
358
+ await tick(LINGER * 3)
359
+
360
+ expect(heard.leave).not.toHaveBeenCalled()
361
+ expect(connections[1].close).not.toHaveBeenCalled()
362
+
363
+ channels[0].quiet.mockImplementation(async () => 0)
364
+
365
+ await tick()
366
+
367
+ expect(heard.leave).toHaveBeenCalledWith(1, '10.0.0.2')
368
+ })
369
+
370
+ it('should count the linger time from when the broker was last busy', async () => {
371
+ const busy = Date.now() + INTERVAL * 2
372
+
373
+ channels[0].quiet.mockImplementation(async () => busy)
374
+
375
+ await tick(LINGER + INTERVAL)
376
+
377
+ expect(heard.leave).not.toHaveBeenCalled()
378
+
379
+ await tick(INTERVAL * 2)
380
+
381
+ expect(heard.leave).toHaveBeenCalled()
382
+ })
383
+
384
+ it('should give up on a broker that is out of reach for the linger time', async () => {
385
+ connections[1].connected = false
386
+ channels[0].quiet.mockImplementation(async () => Date.now())
387
+
388
+ await tick(LINGER + INTERVAL * 2)
389
+
390
+ expect(heard.leave).toHaveBeenCalledWith(1, '10.0.0.2')
391
+ })
392
+
393
+ it('should put a broker that is named again back', async () => {
394
+ addresses.four = '10.0.0.2'
395
+
396
+ await tick(SETTLE + INTERVAL)
397
+
398
+ expect(channels[0].restore).toHaveBeenCalledWith(1)
399
+ expect(heard.join).toHaveBeenCalledWith(1, '10.0.0.2')
400
+
401
+ await tick(LINGER * 2)
402
+
403
+ expect(heard.leave).not.toHaveBeenCalled()
404
+ })
405
+
406
+ it('should retire in a channel made later', async () => {
407
+ const channel = await connection.createChannel(generate())
408
+
409
+ expect(channel.retire).toHaveBeenCalledWith(1)
410
+ })
411
+
412
+ it('should join anew a broker that was left', async () => {
413
+ await tick(LINGER + INTERVAL)
414
+
415
+ addresses.four = '10.0.0.2'
416
+
417
+ await tick(SETTLE + INTERVAL)
418
+
419
+ expect(make).toHaveBeenCalledTimes(4)
420
+ expect(heard.join).toHaveBeenCalledWith(3, '10.0.0.2')
421
+ })
422
+ })
423
+
424
+ it('should follow at once the names of a broker that is out of reach', async () => {
425
+ addresses.three = '10.0.0.3'
426
+ addresses.four = '10.0.0.3'
427
+
428
+ connections[1].connected = false
429
+
430
+ tell(connections[1], 'close', new Error('lost'))
431
+
432
+ await tick(0)
433
+
434
+ expect(make).toHaveBeenCalledWith('amqp://10.0.0.3', 'three')
435
+ expect(heard.join).toHaveBeenCalledWith(2, '10.0.0.3')
436
+ })
437
+
438
+ it('should report a name that fails to resolve', async () => {
439
+ const listener = jest.fn()
440
+ const exception = new Error('ENOTFOUND')
441
+
442
+ connection.diagnose('error', listener)
443
+
444
+ global.COMQ_TESTING_LOOKUP = async () => { throw exception }
445
+
446
+ await tick()
447
+
448
+ expect(listener).toHaveBeenCalledWith(exception)
449
+ expect(heard.retire).not.toHaveBeenCalled()
450
+ })
451
+
452
+ it('should not take a broker that cannot be consumed from for one that has joined', async () => {
453
+ const exception = new Error('PRECONDITION_FAILED')
454
+ const listener = jest.fn()
455
+
456
+ connection.diagnose('error', listener)
457
+ channels[0].join.mockImplementationOnce(async () => { throw exception })
458
+
459
+ addresses.three = '10.0.0.3'
460
+ addresses.four = '10.0.0.3'
461
+
462
+ await tick(SETTLE + INTERVAL)
463
+
464
+ expect(listener).toHaveBeenCalledWith(exception, 2)
465
+ expect(heard.join).not.toHaveBeenCalled()
466
+ expect(heard.retire).not.toHaveBeenCalled()
467
+ expect(channels[0].leave).toHaveBeenCalledWith(2)
468
+ expect(connections[2].close).toHaveBeenCalled()
469
+
470
+ // and joins it anew later
471
+ await tick()
472
+
473
+ expect(heard.join).toHaveBeenCalledWith(3, '10.0.0.3')
474
+ expect(heard.retire).toHaveBeenCalledWith(1, '10.0.0.2')
475
+ })
476
+
477
+ it('should ask again a broker that refused', async () => {
478
+ const exception = new Error('ACCESS-REFUSED')
479
+ const listener = jest.fn()
480
+
481
+ connection.diagnose('error', listener)
482
+
483
+ make.mockImplementationOnce(() => {
484
+ const conn = mock.connection()
485
+
486
+ conn.open.mockImplementation(async () => { throw exception })
487
+ connections.push(conn)
488
+
489
+ return conn
490
+ })
491
+
492
+ addresses.four = '10.0.0.3'
493
+
494
+ await tick(SETTLE + INTERVAL)
495
+
496
+ expect(listener).toHaveBeenCalledWith(exception, 2)
497
+ expect(heard.join).not.toHaveBeenCalled()
498
+
499
+ await tick()
500
+
501
+ expect(heard.join).toHaveBeenCalledWith(3, '10.0.0.3')
502
+ })
503
+ })
@@ -0,0 +1,58 @@
1
+ 'use strict'
2
+
3
+ const { expand } = require('../../source/shards/expand')
4
+
5
+ it('should leave a url without a range as it is', async () => {
6
+ expect(expand('amqp://user:secret@rmq.example.com:5672/vhost?heartbeat=5'))
7
+ .toStrictEqual(['amqp://user:secret@rmq.example.com:5672/vhost?heartbeat=5'])
8
+ })
9
+
10
+ it('should expand a range, its end being exclusive', async () => {
11
+ expect(expand('amqp://rmq[0..3].example.com')).toStrictEqual([
12
+ 'amqp://rmq0.example.com',
13
+ 'amqp://rmq1.example.com',
14
+ 'amqp://rmq2.example.com'
15
+ ])
16
+ })
17
+
18
+ it('should name as many hosts as the end of a range that starts at zero', async () => {
19
+ expect(expand('amqp://rmq[0..32].example.com')).toHaveLength(32)
20
+ })
21
+
22
+ it('should keep what surrounds the host', async () => {
23
+ expect(expand('amqps://user:secret@rmq-[1..3].example.com:5671/vhost?heartbeat=5')).toStrictEqual([
24
+ 'amqps://user:secret@rmq-1.example.com:5671/vhost?heartbeat=5',
25
+ 'amqps://user:secret@rmq-2.example.com:5671/vhost?heartbeat=5'
26
+ ])
27
+ })
28
+
29
+ it('should pad to the width the start is written with', async () => {
30
+ expect(expand('amqp://rmq[08..11]')).toStrictEqual(['amqp://rmq08', 'amqp://rmq09', 'amqp://rmq10'])
31
+ })
32
+
33
+ it('should expand every range', async () => {
34
+ expect(expand('amqp://rmq[0..2].zone[0..2]')).toStrictEqual([
35
+ 'amqp://rmq0.zone0',
36
+ 'amqp://rmq0.zone1',
37
+ 'amqp://rmq1.zone0',
38
+ 'amqp://rmq1.zone1'
39
+ ])
40
+ })
41
+
42
+ it('should leave an IPv6 literal alone', async () => {
43
+ expect(expand('amqp://[::1]:5672')).toStrictEqual(['amqp://[::1]:5672'])
44
+ })
45
+
46
+ it('should look at the host only', async () => {
47
+ const url = 'amqp://user:[0..2]@rmq.example.com/[0..2]?name=[0..2]'
48
+
49
+ expect(expand(url)).toStrictEqual([url])
50
+ })
51
+
52
+ it('should leave what is not a url as it is', async () => {
53
+ expect(expand('whatever')).toStrictEqual(['whatever'])
54
+ })
55
+
56
+ it.each(['[3..3]', '[3..1]'])('should refuse a range that names no hosts: %s', async (range) => {
57
+ expect(() => expand(`amqp://rmq${range}`)).toThrow(RangeError)
58
+ })
@@ -0,0 +1,73 @@
1
+ 'use strict'
2
+
3
+ const { choose, hostOf, locate } = require('../../source/shards/resolve')
4
+
5
+ describe('choose', () => {
6
+ it('should choose the same address whichever order they came in', async () => {
7
+ expect(choose(['10.0.0.2', '10.0.0.1'])).toStrictEqual('10.0.0.1')
8
+ expect(choose(['10.0.0.1', '10.0.0.2'])).toStrictEqual('10.0.0.1')
9
+ })
10
+
11
+ it('should prefer IPv4', async () => {
12
+ expect(choose(['::1', '127.0.0.1'])).toStrictEqual('127.0.0.1')
13
+ })
14
+
15
+ it('should keep the address in use while it is listed', async () => {
16
+ expect(choose(['10.0.0.1', '10.0.0.2'], '10.0.0.2')).toStrictEqual('10.0.0.2')
17
+ })
18
+
19
+ it('should let go of an address that is no longer listed', async () => {
20
+ expect(choose(['10.0.0.1', '10.0.0.3'], '10.0.0.2')).toStrictEqual('10.0.0.1')
21
+ })
22
+ })
23
+
24
+ describe('hostOf', () => {
25
+ it('should tell the name', async () => {
26
+ expect(hostOf('amqp://user:secret@rmq.example.com:5672/vhost')).toStrictEqual('rmq.example.com')
27
+ })
28
+
29
+ it.each(['amqp://10.0.0.1:5672', 'amqp://[::1]', 'whatever'])('should tell nothing of %s',
30
+ async (url) => {
31
+ expect(hostOf(url)).toBeUndefined()
32
+ })
33
+ })
34
+
35
+ describe('locate', () => {
36
+ it('should put the address in place of the name, which the broker is verified by', async () => {
37
+ expect(locate('amqps://user:secret@rmq.example.com:5671/vhost?heartbeat=5', '10.0.0.1')).toStrictEqual({
38
+ url: 'amqps://user:secret@10.0.0.1:5671/vhost?heartbeat=5',
39
+ address: '10.0.0.1',
40
+ servername: 'rmq.example.com'
41
+ })
42
+ })
43
+
44
+ it('should bracket an IPv6 address', async () => {
45
+ expect(locate('amqp://rmq.example.com:5672', '::1').url).toStrictEqual('amqp://[::1]:5672')
46
+ })
47
+
48
+ it('should locate the names of one broker alike', async () => {
49
+ const one = locate('amqp://rmq0.example.com', '10.0.0.1')
50
+ const another = locate('amqp://rmq1.example.com', '10.0.0.1')
51
+
52
+ expect(one.url).toStrictEqual(another.url)
53
+ })
54
+
55
+ it('should tell brokers apart by port, credentials and virtual host', async () => {
56
+ const urls = [
57
+ 'amqp://rmq.example.com',
58
+ 'amqp://rmq.example.com:5673',
59
+ 'amqp://user:secret@rmq.example.com',
60
+ 'amqp://rmq.example.com/vhost'
61
+ ].map((url) => locate(url, '10.0.0.1').url)
62
+
63
+ expect(new Set(urls).size).toStrictEqual(urls.length)
64
+ })
65
+
66
+ it('should leave an address as it is', async () => {
67
+ expect(locate('amqp://10.0.0.1:5672')).toStrictEqual({ url: 'amqp://10.0.0.1:5672', address: '10.0.0.1' })
68
+ })
69
+
70
+ it('should leave what is not a url as it is', async () => {
71
+ expect(locate('whatever')).toStrictEqual({ url: 'whatever', address: 'whatever' })
72
+ })
73
+ })