w-orm-mongodb 1.1.37 → 1.1.39

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.
@@ -0,0 +1,574 @@
1
+ import assert from 'assert'
2
+ import crypto from 'crypto'
3
+ import stream from 'stream'
4
+ import mongodb from 'mongodb'
5
+ import WOrm from '../src/WOrmMongodb.mjs'
6
+ import { genUrl, startContainer, stopContainer } from './lib/api-setup.mjs'
7
+
8
+
9
+ let ctName = 'worm-test-mongodb-gfs'
10
+ let ctDb = 'worm'
11
+ let ctPort = null //容器對外埠, 由startContainer動態取得
12
+
13
+
14
+ //genOpt
15
+ let genOpt = (cl) => {
16
+ return {
17
+ url: genUrl(ctPort),
18
+ db: ctDb,
19
+ cl,
20
+ }
21
+ }
22
+
23
+ //genKeys, 取回傳物件之鍵集合, 用於驗證鍵集合固定
24
+ let genKeys = (v) => {
25
+ return Object.keys(v).sort().join(',')
26
+ }
27
+
28
+ //genU8a, 產生內容可複現之測試數據
29
+ let genU8a = (n, seed = 0) => {
30
+ let u8a = new Uint8Array(n)
31
+ for (let i = 0; i < n; i++) {
32
+ u8a[i] = (i + seed) % 256
33
+ }
34
+ return u8a
35
+ }
36
+
37
+ //genHash
38
+ let genHash = (u8a) => {
39
+ return crypto.createHash('sha256').update(Buffer.from(u8a)).digest('hex')
40
+ }
41
+
42
+ //countDocs, 直接由驅動查GridFS之files與chunks資料表筆數
43
+ let countDocs = async (cl) => {
44
+ let client = new mongodb.MongoClient(genUrl(ctPort))
45
+ let r = null
46
+ try {
47
+ let db = client.db(ctDb)
48
+ r = {
49
+ files: await db.collection(`${cl}.files`).countDocuments({}),
50
+ chunks: await db.collection(`${cl}.chunks`).countDocuments({}),
51
+ }
52
+ }
53
+ finally {
54
+ await client.close()
55
+ }
56
+ return r
57
+ }
58
+
59
+ //uploadByDriver, 直接以驅動上傳指定filename, 用於製造既有數據尚存重複id之情境
60
+ //建立唯一索引後insertGfs已無法製造重複, 故須由驅動直接寫入
61
+ let uploadByDriver = async (cl, filename, u8a) => {
62
+ let client = new mongodb.MongoClient(genUrl(ctPort))
63
+ try {
64
+ let bucket = new mongodb.GridFSBucket(client.db(ctDb), { bucketName: cl })
65
+ await new Promise((resolve, reject) => {
66
+ let rs = new stream.Readable()
67
+ rs._read = () => {}
68
+ rs.push(Buffer.from(u8a))
69
+ rs.push(null)
70
+ rs.pipe(bucket.openUploadStream(filename))
71
+ .on('error', reject)
72
+ .on('finish', resolve)
73
+ })
74
+ }
75
+ finally {
76
+ await client.close()
77
+ }
78
+ }
79
+
80
+
81
+ before(async function() {
82
+ this.timeout(600000) //含拉取映像與服務啟動
83
+ ctPort = await startContainer(ctName)
84
+ })
85
+
86
+ after(async function() {
87
+ this.timeout(120000)
88
+ await stopContainer(ctName)
89
+ })
90
+
91
+
92
+ describe('gfs basic', function() {
93
+ let rt = null
94
+ let vans = {}
95
+ let vget = {}
96
+
97
+ //nSize, 12mb跨過chunkSizeBytes(10mb)故須分為2塊
98
+ let nSize = 12 * 1024 * 1024
99
+
100
+ before(async function () {
101
+ this.timeout(300000)
102
+
103
+ let cl = 'usersGfs'
104
+ let wo = WOrm(genOpt(cl))
105
+
106
+ //on
107
+ wo.on('change', function(mode, data, res) {
108
+ // console.log('change', mode)
109
+ })
110
+
111
+ //u8a
112
+ let u8a = genU8a(nSize)
113
+
114
+ //delAllGfs, 無命中屬正常結果
115
+ rt = null
116
+ // vans[1] = { n: 0, nDeleted: 0, ok: 1 }
117
+ await wo.delAllGfs()
118
+ .then(function(msg) {
119
+ rt = msg
120
+ })
121
+ .catch(function(msg) {
122
+ rt = msg.toString()
123
+ })
124
+ vget[1] = rt
125
+
126
+ //insertGfs
127
+ rt = null
128
+ // vans[2] = { n: 1, nInserted: 1, ok: 1 }
129
+ await wo.insertGfs({ id: 'g-big', u8a })
130
+ .then(function(msg) {
131
+ rt = msg
132
+ })
133
+ .catch(function(msg) {
134
+ rt = msg.toString()
135
+ })
136
+ vget[2] = rt
137
+
138
+ //chunks, 12mb依chunkSizeBytes(10mb)須分為2塊
139
+ // vans[3] = { files: 1, chunks: 2 }
140
+ vget[3] = await countDocs(cl)
141
+
142
+ //selectByPkGfs, 取回內容須與來源逐位元組相同, 且形狀與insertGfs所收之數據物件一致
143
+ rt = null
144
+ // vans[4] = { keys: 'id,u8a', id: 'g-big', length: nSize, hash, b0: 0, b1: 1, b255: 255, b256: 0 }
145
+ await wo.selectByPkGfs('g-big')
146
+ .then(function(msg) {
147
+ rt = {
148
+ keys: genKeys(msg),
149
+ id: msg.id,
150
+ length: msg.u8a.length,
151
+ hash: genHash(msg.u8a),
152
+ b0: msg.u8a[0],
153
+ b1: msg.u8a[1],
154
+ b255: msg.u8a[255],
155
+ b256: msg.u8a[256],
156
+ }
157
+ })
158
+ .catch(function(msg) {
159
+ rt = msg.toString()
160
+ })
161
+ vget[4] = rt
162
+
163
+ //selectByPkGfs by id not existed
164
+ rt = null
165
+ // vans[5] = null
166
+ await wo.selectByPkGfs('id-not-existed')
167
+ .then(function(msg) {
168
+ rt = msg
169
+ })
170
+ .catch(function(msg) {
171
+ rt = `不應reject: ${msg.toString()}`
172
+ })
173
+ vget[5] = rt
174
+
175
+ //selectByPkGfs by id invalid, 未給有效id視為查無數據
176
+ // vans[6] = [null, null, null]
177
+ vget[6] = [
178
+ await wo.selectByPkGfs(''),
179
+ await wo.selectByPkGfs(123),
180
+ await wo.selectByPkGfs(null),
181
+ ]
182
+
183
+ //insertGfs未給id時自動產生
184
+ // vans[7] = { n: 1, nInserted: 1, ok: 1 }
185
+ vget[7] = await wo.insertGfs({ u8a: genU8a(100, 1) })
186
+
187
+ //insertGfs輸入無效視為空結果
188
+ // vans[8] = { n: 0, nInserted: 0, ok: 1 }
189
+ vget[8] = await wo.insertGfs(null)
190
+
191
+ //insertGfs之u8a無效屬呼叫端給值錯誤, 須reject
192
+ rt = null
193
+ // vans[9] = 'Error: invalid data[0].u8a'
194
+ await wo.insertGfs({ id: 'g-bad', u8a: null })
195
+ .then(function(msg) {
196
+ rt = `不應resolve: ${JSON.stringify(msg)}`
197
+ })
198
+ .catch(function(msg) {
199
+ rt = msg.toString()
200
+ })
201
+ vget[9] = rt
202
+
203
+ //delGfs
204
+ rt = null
205
+ // vans[10] = [{ n: 1, nDeleted: 1, ok: 1 }]
206
+ await wo.delGfs({ id: 'g-big' })
207
+ .then(function(msg) {
208
+ rt = msg
209
+ })
210
+ .catch(function(msg) {
211
+ rt = msg.toString()
212
+ })
213
+ vget[10] = rt
214
+
215
+ //delGfs by id not existed, 查無數據屬正常結果
216
+ // vans[11] = [{ n: 0, nDeleted: 0, ok: 1 }]
217
+ vget[11] = await wo.delGfs({ id: 'id-not-existed' })
218
+
219
+ //delGfs未帶有效id, 須以ok為0回報
220
+ // vans[12] = [{ n: 0, nDeleted: 0, ok: 0, err: 'invalid id[undefined]' }]
221
+ vget[12] = await wo.delGfs({ u8a: genU8a(10) })
222
+
223
+ //delGfs輸入無效視為空結果
224
+ // vans[13] = []
225
+ vget[13] = await wo.delGfs(null)
226
+
227
+ //delGfs混合有效與無效id, 須逐筆回報且長度與輸入相同
228
+ await wo.insertGfs({ id: 'g-mix', u8a: genU8a(100, 2) })
229
+ // vans[14] = [{ n: 1, nDeleted: 1, ok: 1 }, { n: 0, nDeleted: 0, ok: 0, err: 'invalid id[undefined]' }]
230
+ vget[14] = await wo.delGfs([{ id: 'g-mix' }, { u8a: genU8a(10) }])
231
+
232
+ //鍵集合須固定
233
+ // vans[15] = ['n,nDeleted,ok', 'n,nDeleted,ok', 'err,n,nDeleted,ok']
234
+ vget[15] = [genKeys(vget[10][0]), genKeys(vget[11][0]), genKeys(vget[12][0])]
235
+
236
+ //delAllGfs, 先插入2筆再全刪
237
+ await wo.insertGfs([
238
+ { id: 'g-a', u8a: genU8a(100, 3) },
239
+ { id: 'g-b', u8a: genU8a(100, 4) },
240
+ ])
241
+ rt = null
242
+ // vans[16] = { n: 3, nDeleted: 3, ok: 1 } //含未給id而自動產生之該筆
243
+ await wo.delAllGfs()
244
+ .then(function(msg) {
245
+ rt = msg
246
+ })
247
+ .catch(function(msg) {
248
+ rt = msg.toString()
249
+ })
250
+ vget[16] = rt
251
+
252
+ //全刪後files與chunks須一併清除
253
+ // vans[17] = { files: 0, chunks: 0 }
254
+ vget[17] = await countDocs(cl)
255
+
256
+ //insertGfs之鍵集合須固定
257
+ // vans[18] = ['n,nInserted,ok', 'n,nInserted,ok']
258
+ vget[18] = [genKeys(vget[2]), genKeys(vget[8])]
259
+
260
+ })
261
+
262
+ vans[1] = { n: 0, nDeleted: 0, ok: 1 }
263
+ it(`should get ${JSON.stringify(vans[1])} for delAllGfs with no matched data`, async function() {
264
+ assert.strict.deepStrictEqual(vget[1], vans[1])
265
+ })
266
+
267
+ vans[2] = { n: 1, nInserted: 1, ok: 1 }
268
+ it(`should get ${JSON.stringify(vans[2])} for insertGfs`, async function() {
269
+ assert.strict.deepStrictEqual(vget[2], vans[2])
270
+ })
271
+
272
+ vans[3] = { files: 1, chunks: 2 }
273
+ it(`should get ${JSON.stringify(vans[3])} for chunks of 12mb data by chunkSizeBytes 10mb`, async function() {
274
+ assert.strict.deepStrictEqual(vget[3], vans[3])
275
+ })
276
+
277
+ vans[4] = {
278
+ keys: 'id,u8a',
279
+ id: 'g-big',
280
+ length: nSize,
281
+ hash: genHash(genU8a(nSize)),
282
+ b0: 0,
283
+ b1: 1,
284
+ b255: 255,
285
+ b256: 0,
286
+ }
287
+ it(`should get same content for selectByPkGfs`, async function() {
288
+ assert.strict.deepStrictEqual(vget[4], vans[4])
289
+ })
290
+
291
+ vans[5] = null
292
+ it(`should get ${JSON.stringify(vans[5])} for selectByPkGfs by id not existed`, async function() {
293
+ assert.strict.deepStrictEqual(vget[5], vans[5])
294
+ })
295
+
296
+ vans[6] = [null, null, null]
297
+ it(`should get ${JSON.stringify(vans[6])} for selectByPkGfs by id invalid`, async function() {
298
+ assert.strict.deepStrictEqual(vget[6], vans[6])
299
+ })
300
+
301
+ vans[7] = { n: 1, nInserted: 1, ok: 1 }
302
+ it(`should get ${JSON.stringify(vans[7])} for insertGfs without id`, async function() {
303
+ assert.strict.deepStrictEqual(vget[7], vans[7])
304
+ })
305
+
306
+ vans[8] = { n: 0, nInserted: 0, ok: 1 }
307
+ it(`should get ${JSON.stringify(vans[8])} for insertGfs with invalid data`, async function() {
308
+ assert.strict.deepStrictEqual(vget[8], vans[8])
309
+ })
310
+
311
+ vans[9] = 'Error: invalid data[0].u8a'
312
+ it(`should get ${JSON.stringify(vans[9])} for insertGfs with invalid u8a`, async function() {
313
+ assert.strict.deepStrictEqual(vget[9], vans[9])
314
+ })
315
+
316
+ vans[10] = [{ n: 1, nDeleted: 1, ok: 1 }]
317
+ it(`should get ${JSON.stringify(vans[10])} for delGfs`, async function() {
318
+ assert.strict.deepStrictEqual(vget[10], vans[10])
319
+ })
320
+
321
+ vans[11] = [{ n: 0, nDeleted: 0, ok: 1 }]
322
+ it(`should get ${JSON.stringify(vans[11])} for delGfs by id not existed`, async function() {
323
+ assert.strict.deepStrictEqual(vget[11], vans[11])
324
+ })
325
+
326
+ vans[12] = [{ n: 0, nDeleted: 0, ok: 0, err: 'invalid id[undefined]' }]
327
+ it(`should get ${JSON.stringify(vans[12])} for delGfs without valid id`, async function() {
328
+ assert.strict.deepStrictEqual(vget[12], vans[12])
329
+ })
330
+
331
+ vans[13] = []
332
+ it(`should get ${JSON.stringify(vans[13])} for delGfs with invalid data`, async function() {
333
+ assert.strict.deepStrictEqual(vget[13], vans[13])
334
+ })
335
+
336
+ vans[14] = [
337
+ { n: 1, nDeleted: 1, ok: 1 },
338
+ { n: 0, nDeleted: 0, ok: 0, err: 'invalid id[undefined]' }
339
+ ]
340
+ it(`should get ${JSON.stringify(vans[14])} for delGfs with mixed valid and invalid id`, async function() {
341
+ assert.strict.deepStrictEqual(vget[14], vans[14])
342
+ })
343
+
344
+ vans[15] = ['n,nDeleted,ok', 'n,nDeleted,ok', 'err,n,nDeleted,ok']
345
+ it(`should get ${JSON.stringify(vans[15])} for fixed key set of delGfs`, async function() {
346
+ assert.strict.deepStrictEqual(vget[15], vans[15])
347
+ })
348
+
349
+ vans[16] = { n: 3, nDeleted: 3, ok: 1 }
350
+ it(`should get ${JSON.stringify(vans[16])} for delAllGfs`, async function() {
351
+ assert.strict.deepStrictEqual(vget[16], vans[16])
352
+ })
353
+
354
+ vans[17] = { files: 0, chunks: 0 }
355
+ it(`should get ${JSON.stringify(vans[17])} for files and chunks after delAllGfs`, async function() {
356
+ assert.strict.deepStrictEqual(vget[17], vans[17])
357
+ })
358
+
359
+ vans[18] = ['n,nInserted,ok', 'n,nInserted,ok']
360
+ it(`should get ${JSON.stringify(vans[18])} for fixed key set of insertGfs`, async function() {
361
+ assert.strict.deepStrictEqual(vget[18], vans[18])
362
+ })
363
+
364
+ })
365
+
366
+
367
+ describe('gfs insert', function() {
368
+ let vans = {}
369
+ let vget = {}
370
+
371
+ before(async function() {
372
+ this.timeout(300000)
373
+
374
+ let cl = 'insertGfs'
375
+ let wo = WOrm(genOpt(cl))
376
+ await wo.delAllGfs()
377
+
378
+ //全新3筆
379
+ vget[1] = await wo.insertGfs([
380
+ { id: 'i1', u8a: genU8a(100, 1) },
381
+ { id: 'i2', u8a: genU8a(100, 2) },
382
+ { id: 'i3', u8a: genU8a(100, 3) },
383
+ ])
384
+ let c1 = await countDocs(cl)
385
+
386
+ //重複插入同3筆, 全數已存在故皆不插入, 且不得reject
387
+ vget[2] = await wo.insertGfs([
388
+ { id: 'i1', u8a: genU8a(100, 1) },
389
+ { id: 'i2', u8a: genU8a(100, 2) },
390
+ { id: 'i3', u8a: genU8a(100, 3) },
391
+ ])
392
+
393
+ //重複插入不得殘留孤兒chunks, 亦不得新增files
394
+ let c2 = await countDocs(cl)
395
+ vget[3] = { files: c2.files - c1.files, chunks: c2.chunks - c1.chunks }
396
+
397
+ //已存在id不得被覆寫, 內容須為首次所給
398
+ let vi1 = await wo.selectByPkGfs('i1')
399
+ vget[4] = genHash(vi1.u8a) === genHash(genU8a(100, 1))
400
+
401
+ //部份重複, 1筆已存在2筆為新
402
+ vget[5] = await wo.insertGfs([
403
+ { id: 'i2', u8a: genU8a(100, 2) },
404
+ { id: 'i4', u8a: genU8a(100, 4) },
405
+ { id: 'i5', u8a: genU8a(100, 5) },
406
+ ])
407
+
408
+ //同批含重複id, 僅首筆成功且保留首筆內容
409
+ vget[6] = await wo.insertGfs([
410
+ { id: 'i-dup', u8a: genU8a(100, 6) },
411
+ { id: 'i-dup', u8a: genU8a(100, 7) },
412
+ { id: 'i-uniq', u8a: genU8a(100, 8) },
413
+ ])
414
+ let vdup = await wo.selectByPkGfs('i-dup')
415
+ vget[7] = genHash(vdup.u8a) === genHash(genU8a(100, 6))
416
+
417
+ //併發對同一id插入10次, nInserted總和須為1
418
+ let c3 = await countDocs(cl)
419
+ let rs = await Promise.all(Array.from({ length: 10 }, (v, k) => {
420
+ return wo.insertGfs({ id: 'i-race', u8a: genU8a(100, k) })
421
+ }))
422
+ vget[8] = rs.reduce((sum, v) => sum + v.nInserted, 0)
423
+ vget[9] = rs.filter((v) => v.ok !== 1).length
424
+
425
+ //併發後僅新增1筆files, 且失敗者不得殘留孤兒chunks
426
+ let c4 = await countDocs(cl)
427
+ vget[10] = { files: c4.files - c3.files, chunks: c4.chunks - c3.chunks }
428
+
429
+ //最終筆數, 3+0+2+2+1=8
430
+ vget[11] = (await countDocs(cl)).files
431
+
432
+ })
433
+
434
+ vans[1] = { n: 3, nInserted: 3, ok: 1 }
435
+ it(`should get ${JSON.stringify(vans[1])} for insertGfs 3 new records`, async function() {
436
+ assert.strict.deepStrictEqual(vget[1], vans[1])
437
+ })
438
+
439
+ vans[2] = { n: 3, nInserted: 0, ok: 1 }
440
+ it(`should get ${JSON.stringify(vans[2])} for insertGfs 3 existed records`, async function() {
441
+ assert.strict.deepStrictEqual(vget[2], vans[2])
442
+ })
443
+
444
+ vans[3] = { files: 0, chunks: 0 }
445
+ it(`should get ${JSON.stringify(vans[3])} for no orphan chunks after insertGfs existed records`, async function() {
446
+ assert.strict.deepStrictEqual(vget[3], vans[3])
447
+ })
448
+
449
+ vans[4] = true
450
+ it(`should get ${JSON.stringify(vans[4])} for not overwriting existed id`, async function() {
451
+ assert.strict.deepStrictEqual(vget[4], vans[4])
452
+ })
453
+
454
+ vans[5] = { n: 3, nInserted: 2, ok: 1 }
455
+ it(`should get ${JSON.stringify(vans[5])} for insertGfs 1 existed and 2 new records`, async function() {
456
+ assert.strict.deepStrictEqual(vget[5], vans[5])
457
+ })
458
+
459
+ vans[6] = { n: 3, nInserted: 2, ok: 1 }
460
+ it(`should get ${JSON.stringify(vans[6])} for insertGfs with duplicated id in same batch`, async function() {
461
+ assert.strict.deepStrictEqual(vget[6], vans[6])
462
+ })
463
+
464
+ vans[7] = true
465
+ it(`should get ${JSON.stringify(vans[7])} for keeping first one of duplicated id in same batch`, async function() {
466
+ assert.strict.deepStrictEqual(vget[7], vans[7])
467
+ })
468
+
469
+ vans[8] = 1
470
+ it(`should get ${JSON.stringify(vans[8])} for sum of nInserted by 10 concurrent insertGfs with same id`, async function() {
471
+ assert.strict.deepStrictEqual(vget[8], vans[8])
472
+ })
473
+
474
+ vans[9] = 0
475
+ it(`should get ${JSON.stringify(vans[9])} for count of ok!==1 in 10 concurrent insertGfs`, async function() {
476
+ assert.strict.deepStrictEqual(vget[9], vans[9])
477
+ })
478
+
479
+ vans[10] = { files: 1, chunks: 1 }
480
+ it(`should get ${JSON.stringify(vans[10])} for no orphan chunks after 10 concurrent insertGfs`, async function() {
481
+ assert.strict.deepStrictEqual(vget[10], vans[10])
482
+ })
483
+
484
+ vans[11] = 8
485
+ it(`should get ${JSON.stringify(vans[11])} for files after all insertGfs`, async function() {
486
+ assert.strict.deepStrictEqual(vget[11], vans[11])
487
+ })
488
+
489
+ })
490
+
491
+
492
+ describe('gfs legacy duplicated id', function() {
493
+ let vans = {}
494
+ let vget = {}
495
+
496
+ before(async function() {
497
+ this.timeout(300000)
498
+
499
+ //既有數據尚存重複id之情境, 以驅動直接製造, 並於本資料表不呼叫insertGfs以免建立唯一索引
500
+ let cl = 'legacyGfs'
501
+ let wo = WOrm(genOpt(cl))
502
+ await wo.delAllGfs()
503
+ await uploadByDriver(cl, 'l-dup', genU8a(100, 1))
504
+ await uploadByDriver(cl, 'l-dup', genU8a(100, 2))
505
+ vget[1] = (await countDocs(cl)).files
506
+
507
+ //delGfs須一併刪除並如實回報nDeleted
508
+ vget[2] = await wo.delGfs({ id: 'l-dup' })
509
+ vget[3] = await countDocs(cl)
510
+
511
+ })
512
+
513
+ vans[1] = 2
514
+ it(`should get ${JSON.stringify(vans[1])} for files of legacy duplicated id`, async function() {
515
+ assert.strict.deepStrictEqual(vget[1], vans[1])
516
+ })
517
+
518
+ vans[2] = [{ n: 1, nDeleted: 2, ok: 1 }]
519
+ it(`should get ${JSON.stringify(vans[2])} for delGfs with legacy duplicated id`, async function() {
520
+ assert.strict.deepStrictEqual(vget[2], vans[2])
521
+ })
522
+
523
+ vans[3] = { files: 0, chunks: 0 }
524
+ it(`should get ${JSON.stringify(vans[3])} for files and chunks after delGfs with legacy duplicated id`, async function() {
525
+ assert.strict.deepStrictEqual(vget[3], vans[3])
526
+ })
527
+
528
+ })
529
+
530
+
531
+ describe('gfs change event', function() {
532
+ let vans = {}
533
+ let vget = {}
534
+
535
+ before(async function() {
536
+ this.timeout(300000)
537
+
538
+ let cl = 'evGfs'
539
+ let wo = WOrm(genOpt(cl))
540
+ await wo.delAllGfs()
541
+
542
+ //訂閱函數拋錯不得影響本次操作之結果
543
+ wo.on('change', function() {
544
+ throw new Error('訂閱者拋錯')
545
+ })
546
+
547
+ vget[1] = await wo.insertGfs({ id: 'e1', u8a: genU8a(100, 1) })
548
+ vget[2] = genHash((await wo.selectByPkGfs('e1')).u8a) === genHash(genU8a(100, 1))
549
+ vget[3] = await wo.delGfs({ id: 'e1' })
550
+ vget[4] = await wo.delAllGfs()
551
+
552
+ })
553
+
554
+ vans[1] = { n: 1, nInserted: 1, ok: 1 }
555
+ it(`should get ${JSON.stringify(vans[1])} for insertGfs with throwing listener`, async function() {
556
+ assert.strict.deepStrictEqual(vget[1], vans[1])
557
+ })
558
+
559
+ vans[2] = true
560
+ it(`should get ${JSON.stringify(vans[2])} for content after insertGfs with throwing listener`, async function() {
561
+ assert.strict.deepStrictEqual(vget[2], vans[2])
562
+ })
563
+
564
+ vans[3] = [{ n: 1, nDeleted: 1, ok: 1 }]
565
+ it(`should get ${JSON.stringify(vans[3])} for delGfs with throwing listener`, async function() {
566
+ assert.strict.deepStrictEqual(vget[3], vans[3])
567
+ })
568
+
569
+ vans[4] = { n: 0, nDeleted: 0, ok: 1 }
570
+ it(`should get ${JSON.stringify(vans[4])} for delAllGfs with throwing listener`, async function() {
571
+ assert.strict.deepStrictEqual(vget[4], vans[4])
572
+ })
573
+
574
+ })
@@ -0,0 +1,142 @@
1
+ import net from 'net'
2
+ import mongodb from 'mongodb'
3
+ import { execFile } from 'child_process'
4
+ import { promisify } from 'util'
5
+
6
+
7
+ //測試自行創建docker容器供MongoDB服務, 測試結束即銷毀, 故僅需環境已安裝docker
8
+
9
+
10
+ let execFileAsync = promisify(execFile)
11
+
12
+
13
+ let ctImage = 'mongo:8'
14
+ let ctUser = 'username'
15
+ let ctPassword = 'password'
16
+
17
+
18
+ let delay = (ms) => {
19
+ return new Promise((resolve) => {
20
+ setTimeout(resolve, ms)
21
+ })
22
+ }
23
+
24
+ //getFreePort, 由系統配發空閒埠
25
+ let getFreePort = () => {
26
+ return new Promise((resolve, reject) => {
27
+ let srv = net.createServer()
28
+ srv.on('error', reject)
29
+ srv.listen(0, () => {
30
+ let port = srv.address().port
31
+ srv.close(() => {
32
+ resolve(port)
33
+ })
34
+ })
35
+ })
36
+ }
37
+
38
+ //docker, 以陣列傳參不經shell, 避免路徑與引號轉譯問題
39
+ let docker = (args) => {
40
+ return execFileAsync('docker', args)
41
+ }
42
+
43
+ //genUrl
44
+ let genUrl = (port) => {
45
+ return `mongodb://${ctUser}:${ctPassword}@127.0.0.1:${port}`
46
+ }
47
+
48
+ //probeReady, 以mongodb驅動直連容器對外埠試跑ping
49
+ //不使用容器內mongosh, 因mongo映像初始化期間會先啟動僅監聽unix socket之臨時服務,
50
+ //容器內探測會提早回報就緒, 直連TCP方為測試實際所走路徑
51
+ let probeReady = async (port) => {
52
+ let client = new mongodb.MongoClient(genUrl(port), { serverSelectionTimeoutMS: 2000 })
53
+ let ok = false
54
+ try {
55
+ await client.db('admin').command({ ping: 1 })
56
+ ok = true
57
+ }
58
+ catch (err) {
59
+ ok = false
60
+ }
61
+ finally {
62
+ await client.close().catch(() => {})
63
+ }
64
+ return ok
65
+ }
66
+
67
+ //startContainer, 創建容器並等待服務就緒, 回傳容器對外埠
68
+ //對外埠由getFreePort動態取得, 避免與既有MongoDB服務搶27017, 亦避免多測試檔並行時互搶同一埠
69
+ //mongo映像以MONGO_INITDB_ROOT_USERNAME、MONGO_INITDB_ROOT_PASSWORD即建妥帳號, 無須另跑建置語句
70
+ let startContainer = async (ctName) => {
71
+
72
+ //check docker
73
+ await docker(['version', '--format', '{{.Server.Version}}'])
74
+ .catch(() => {
75
+ throw new Error('需先安裝並啟動docker才能執行mongodb測試')
76
+ })
77
+
78
+ //清除前次殘留容器
79
+ await docker(['rm', '-f', ctName]).catch(() => {})
80
+
81
+ //run, getFreePort取得之埠於關閉監聽至docker實際綁定之間可能已被他者佔用,
82
+ //多測試檔並行時尤其容易發生, 故綁定失敗即改取新埠重試
83
+ let ctPort = null
84
+ let nTry = 10
85
+ for (let i = 0; i < nTry; i++) {
86
+
87
+ //ctPort
88
+ ctPort = await getFreePort()
89
+
90
+ //run
91
+ let err = null
92
+ await docker(['run', '-d', '--rm', '--name', ctName, '-e', `MONGO_INITDB_ROOT_USERNAME=${ctUser}`, '-e', `MONGO_INITDB_ROOT_PASSWORD=${ctPassword}`, '-p', `${ctPort}:27017`, ctImage])
93
+ .catch((e) => {
94
+ err = e
95
+ })
96
+
97
+ //check
98
+ if (err === null) {
99
+ break
100
+ }
101
+
102
+ //僅埠被佔用方可重試, 其餘錯誤須往外拋
103
+ let msg = `${err.stderr || ''}${err.message || ''}`
104
+ let isPortBusy = msg.indexOf('address already in use') >= 0 || msg.indexOf('port is already allocated') >= 0
105
+ if (!isPortBusy || i === nTry - 1) {
106
+ throw err
107
+ }
108
+
109
+ //清除綁定失敗所遺留之容器
110
+ await docker(['rm', '-f', ctName]).catch(() => {})
111
+
112
+ }
113
+
114
+ //等待服務就緒, 首次需拉取映像故給予較長時間
115
+ let ready = false
116
+ for (let i = 0; i < 90; i++) {
117
+ let ok = await probeReady(ctPort)
118
+ if (ok) {
119
+ ready = true
120
+ break
121
+ }
122
+ await delay(2000)
123
+ }
124
+ if (!ready) {
125
+ throw new Error('mongodb容器啟動逾時')
126
+ }
127
+
128
+ return ctPort
129
+ }
130
+
131
+ //stopContainer, 銷毀容器
132
+ let stopContainer = async (ctName) => {
133
+ await docker(['rm', '-f', ctName]).catch(() => {})
134
+ }
135
+
136
+
137
+ export {
138
+ delay,
139
+ genUrl,
140
+ startContainer,
141
+ stopContainer
142
+ }