w-orm-mongodb 1.2.5 → 1.2.7

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,489 @@
1
+ import assert from 'assert'
2
+ import mongodb from 'mongodb'
3
+ import WOrm from '../src/WOrmMongodb.mjs'
4
+ import cvOrmFind from 'wsemi/src/cvOrmFind.mjs'
5
+ import cvOrmFindCases from 'wsemi/src/_cvOrmFindCases.mjs'
6
+ import { delay, genUrl, startContainer, stopContainer } from './lib/api-setup.mjs'
7
+
8
+
9
+ //GridFS之條件(find)白名單接線測試, 規則見CLAUDE_rulebook.md R-FIND-01
10
+ //delAllGfs與_findGfs(含delGfs逐筆之{filename:id})適用同一白名單, 檔案清單一律由驅動直接查詢, 不經受測套件
11
+
12
+
13
+ let { cases } = cvOrmFindCases
14
+
15
+
16
+ let ctName = 'worm-test-mongodb-find-whitelist-gfs'
17
+ let ctDb = 'worm'
18
+ let ctPort = null //容器對外埠, 由startContainer動態取得
19
+ let rawClient = null
20
+
21
+
22
+ let ids = ['g1', 'g2', 'g3', 'g4', 'g5', 'g6']
23
+
24
+
25
+ //genOpt
26
+ let genOpt = (cl) => {
27
+ return {
28
+ url: genUrl(ctPort),
29
+ db: ctDb,
30
+ cl,
31
+ }
32
+ }
33
+
34
+ //genU8a, 產生內容可複現之測試數據
35
+ let genU8a = (n, seed = 0) => {
36
+ let u8a = new Uint8Array(n)
37
+ for (let i = 0; i < n; i++) {
38
+ u8a[i] = (i + seed) % 256
39
+ }
40
+ return u8a
41
+ }
42
+
43
+ //listNames, 以驅動查files資料表之filename清單(升冪)
44
+ let listNames = async (cl) => {
45
+ let rs = await rawClient.db(ctDb).collection(`${cl}.files`).find({}, { projection: { _id: 0, filename: 1 } }).sort({ filename: 1 }).toArray()
46
+ return rs.map((v) => v.filename)
47
+ }
48
+
49
+ //countChunks, 以驅動查chunks資料表筆數
50
+ let countChunks = async (cl) => {
51
+ return rawClient.db(ctDb).collection(`${cl}.chunks`).countDocuments({})
52
+ }
53
+
54
+ //reseed, 以驅動清空後經insertGfs寫入6個檔案(g1~g6, 長度10~60)
55
+ let reseed = async (cl, wo) => {
56
+ await rawClient.db(ctDb).collection(`${cl}.files`).deleteMany({})
57
+ await rawClient.db(ctDb).collection(`${cl}.chunks`).deleteMany({})
58
+ let r = await wo.insertGfs(ids.map((id, i) => {
59
+ return { id, u8a: genU8a((i + 1) * 10, i) }
60
+ }))
61
+ assert.strict.deepStrictEqual(r.nInserted, 6)
62
+ }
63
+
64
+ //collect, 蒐集事件(依發生順序)
65
+ let collect = (wo) => {
66
+ let evs = []
67
+ wo.on('error', function(mode, data, err) {
68
+ evs.push({ type: 'error', mode, data, err })
69
+ })
70
+ wo.on('change', function(mode, data, res) {
71
+ evs.push({ type: 'change', mode, data, res })
72
+ })
73
+ return evs
74
+ }
75
+
76
+ //nType, 事件陣列內指定類型之筆數
77
+ let nType = (evs, type) => {
78
+ return evs.filter((v) => v.type === type).length
79
+ }
80
+
81
+ //getMsg, 取共用檢核對該find之錯誤訊息(本套件須原樣reject該訊息)
82
+ let getMsg = (find) => {
83
+ let msg = null
84
+ try {
85
+ cvOrmFind(find)
86
+ }
87
+ catch (err) {
88
+ msg = err.message
89
+ }
90
+ return msg
91
+ }
92
+
93
+ //withSpyClient, 於建立受測物件期間以計數替身取代MongoClient(受測套件於建立時取用MongoClient), 完成即還原
94
+ let withSpyClient = (fn) => {
95
+ let Orig = mongodb.MongoClient
96
+ let desc = Object.getOwnPropertyDescriptor(mongodb, 'MongoClient')
97
+ let count = { n: 0 }
98
+ class Spy extends Orig {
99
+ constructor(...args) {
100
+ super(...args)
101
+ count.n++
102
+ }
103
+ }
104
+ Object.defineProperty(mongodb, 'MongoClient', { configurable: true, enumerable: true, get: () => Spy })
105
+ let wo = null
106
+ try {
107
+ wo = fn()
108
+ }
109
+ finally {
110
+ Object.defineProperty(mongodb, 'MongoClient', desc)
111
+ }
112
+ return { wo, count }
113
+ }
114
+
115
+ //genFlipSpy, 產生包裝target之Proxy, 其key欄僅第一次被讀取時為v1, 其後(含get陷阱)皆為v2(用於驗證檢查之值即使用之值),
116
+ //回傳{ proxy, stat }, stat記錄key被屬性描述子讀取(desc)與被get讀取(get)之次數: 檢核須只以描述子讀取一次, 其後不得再讀取原物件
117
+ let genFlipSpy = (target, key, v1, v2) => {
118
+ let stat = { desc: 0, get: 0 }
119
+ let n = 0
120
+ let proxy = new Proxy(target, {
121
+ get(t, k, r) {
122
+ if (k === key) {
123
+ stat.get++
124
+ n++
125
+ return n === 1 ? v1 : v2
126
+ }
127
+ return Reflect.get(t, k, r)
128
+ },
129
+ getOwnPropertyDescriptor(t, k) {
130
+ let d = Reflect.getOwnPropertyDescriptor(t, k)
131
+ if (k === key) {
132
+ stat.desc++
133
+ n++
134
+ d.value = n === 1 ? v1 : v2
135
+ }
136
+ return d
137
+ },
138
+ })
139
+ return { proxy, stat }
140
+ }
141
+
142
+ //genFlips, 翻轉案例: 條件內各層(頂層欄、運算子物件、$in陣列元素、$or元素內欄位)各有一處被翻轉, 檢核後之複本須為第一次讀取之值(皆只刪除g1)
143
+ let genFlips = () => {
144
+ let r = []
145
+ let add = (label, fn) => {
146
+ r.push({ label, gen: fn })
147
+ }
148
+ add('top level field', () => {
149
+ let s = genFlipSpy({ filename: 'g1' }, 'filename', 'g1', 'g2')
150
+ return { find: s.proxy, stats: [s.stat] }
151
+ })
152
+ add('operator object $in', () => {
153
+ let s = genFlipSpy({ $in: ['g1'] }, '$in', ['g1'], ['g2'])
154
+ return { find: { filename: s.proxy }, stats: [s.stat] }
155
+ })
156
+ add('element of $in array', () => {
157
+ let s = genFlipSpy(['g1'], '0', 'g1', 'g2')
158
+ return { find: { filename: { $in: s.proxy } }, stats: [s.stat] }
159
+ })
160
+ add('field in element of $or array', () => {
161
+ let s = genFlipSpy({ filename: 'g1' }, 'filename', 'g1', 'g2')
162
+ return { find: { $or: [s.proxy] }, stats: [s.stat] }
163
+ })
164
+ return r
165
+ }
166
+
167
+
168
+ before(async function() {
169
+ this.timeout(600000) //含拉取映像與服務啟動
170
+ ctPort = await startContainer(ctName)
171
+ rawClient = new mongodb.MongoClient(genUrl(ctPort))
172
+ await rawClient.connect()
173
+ })
174
+
175
+ after(async function() {
176
+ this.timeout(120000)
177
+ if (rawClient) {
178
+ await rawClient.close()
179
+ }
180
+ await stopContainer(ctName)
181
+ })
182
+
183
+
184
+ describe('find whitelist gfs: reject', function() {
185
+ let cl = 'gfsrej'
186
+ let wo = null
187
+ let evs = null
188
+ let rejs = cases.filter((c) => c.expect === 'REJ')
189
+
190
+ before(async function() {
191
+ this.timeout(180000)
192
+ wo = WOrm(genOpt(cl))
193
+ evs = collect(wo)
194
+ await reseed(cl, wo)
195
+ })
196
+
197
+ for (let c of rejs) {
198
+
199
+ it(`should get "invalid find" and keep files for delAllGfs ${c.id} ${c.label}`, async function() {
200
+ this.timeout(180000)
201
+ let names0 = await listNames(cl)
202
+ let chunks0 = await countChunks(cl)
203
+ let msgExp = getMsg(c.find())
204
+ evs.length = 0
205
+
206
+ //call
207
+ let err = null
208
+ let nErrAtReject = -1
209
+ let nChgAtReject = -1
210
+ await wo.delAllGfs(c.find())
211
+ .catch(function(e) {
212
+ err = e
213
+ nErrAtReject = nType(evs, 'error')
214
+ nChgAtReject = nType(evs, 'change')
215
+ })
216
+ await delay(30)
217
+
218
+ //檔案須不變, 若被刪則先還原以免影響後續案例
219
+ let names1 = await listNames(cl)
220
+ let chunks1 = await countChunks(cl)
221
+ let same = JSON.stringify(names1) === JSON.stringify(names0) && chunks1 === chunks0
222
+ if (!same) {
223
+ await reseed(cl, wo)
224
+ }
225
+
226
+ //reject, 訊息原樣(不包裝)
227
+ assert.ok(err instanceof Error, 'delAllGfs should reject')
228
+ assert.strict.deepStrictEqual(typeof msgExp, 'string')
229
+ assert.strict.deepStrictEqual(msgExp.indexOf('invalid find: '), 0)
230
+ assert.strict.deepStrictEqual(err.message, msgExp)
231
+
232
+ //error事件須於reject之前恰發出一次, change不發
233
+ assert.strict.deepStrictEqual(nErrAtReject, 1)
234
+ assert.strict.deepStrictEqual(nChgAtReject, 0)
235
+ assert.strict.deepStrictEqual(nType(evs, 'error'), 1)
236
+ assert.strict.deepStrictEqual(nType(evs, 'change'), 0)
237
+ assert.strict.deepStrictEqual(evs[0].mode, 'delAllGfs')
238
+ assert.strict.deepStrictEqual(evs[0].data, null)
239
+ assert.strict.deepStrictEqual(evs[0].err, msgExp)
240
+
241
+ //檔案不變
242
+ assert.strict.deepStrictEqual(same, true)
243
+
244
+ })
245
+
246
+ }
247
+
248
+ it('should reject GridFS specific forms: $regex, dotted path, $where', async function() {
249
+ this.timeout(180000)
250
+ await reseed(cl, wo)
251
+ let finds = [
252
+ { filename: { $regex: '^g' } },
253
+ { 'metadata.owner': 'x' },
254
+ { filename: 'g1', $where: 'true' },
255
+ { filename: /g1/ },
256
+ { $nor: [{ filename: 'g1' }] },
257
+ { length: { $exists: true } },
258
+ ]
259
+ for (let find of finds) {
260
+ let err = null
261
+ await wo.delAllGfs(find)
262
+ .catch(function(e) {
263
+ err = e
264
+ })
265
+ assert.ok(err instanceof Error)
266
+ assert.strict.deepStrictEqual(err.message.indexOf('invalid find: '), 0)
267
+ }
268
+ assert.strict.deepStrictEqual(await listNames(cl), ids)
269
+ })
270
+
271
+ })
272
+
273
+
274
+ describe('find whitelist gfs: accept', function() {
275
+ let cl = 'gfsok'
276
+ let wo = null
277
+ let evs = null
278
+ let future = () => new Date(Date.now() + 3600 * 1000)
279
+
280
+ before(function() {
281
+ wo = WOrm(genOpt(cl))
282
+ evs = collect(wo)
283
+ })
284
+
285
+ let oks = [
286
+ { label: 'no find', find: () => undefined, del: ids, call: 'noarg' },
287
+ { label: 'undefined', find: () => undefined, del: ids },
288
+ { label: '{}', find: () => ({}), del: ids },
289
+ { label: 'null prototype {}', find: () => Object.create(null), del: ids },
290
+ { label: '{filename:g2}', find: () => ({ filename: 'g2' }), del: ['g2'] },
291
+ { label: '{filename:not-existed}', find: () => ({ filename: 'not-existed' }), del: [] },
292
+ { label: '{filename:{$in:[g1,g3]}}', find: () => ({ filename: { $in: ['g1', 'g3'] } }), del: ['g1', 'g3'] },
293
+ { label: '{filename:{$in:[]}} always false', find: () => ({ filename: { $in: [] } }), del: [] },
294
+ { label: '{filename:{$nin:[g1]}}', find: () => ({ filename: { $nin: ['g1'] } }), del: ['g2', 'g3', 'g4', 'g5', 'g6'] },
295
+ { label: '{$or:[{filename:g1},{filename:g6}]}', find: () => ({ $or: [{ filename: 'g1' }, { filename: 'g6' }] }), del: ['g1', 'g6'] },
296
+ { label: '{$and:[{filename:{$ne:g1}},{length:{$gt:30}}]}', find: () => ({ $and: [{ filename: { $ne: 'g1' } }, { length: { $gt: 30 } }] }), del: ['g4', 'g5', 'g6'] },
297
+ { label: '{length:{$gte:40}}', find: () => ({ length: { $gte: 40 } }), del: ['g4', 'g5', 'g6'] },
298
+ { label: '{length:{$lt:30,$gt:10}}', find: () => ({ length: { $lt: 30, $gt: 10 } }), del: ['g2'] },
299
+ { label: '{length:20}', find: () => ({ length: 20 }), del: ['g2'] },
300
+ { label: '{uploadDate:{$lt:future}}', find: () => ({ uploadDate: { $lt: future() } }), del: ids },
301
+ { label: '{uploadDate:{$gt:future}}', find: () => ({ uploadDate: { $gt: future() } }), del: [] },
302
+ ]
303
+
304
+ for (let c of oks) {
305
+
306
+ it(`should delete ${JSON.stringify(c.del)} for delAllGfs ${c.label}`, async function() {
307
+ this.timeout(180000)
308
+ await reseed(cl, wo)
309
+ evs.length = 0
310
+
311
+ //delAllGfs
312
+ let r = null
313
+ if (c.call === 'noarg') {
314
+ r = await wo.delAllGfs()
315
+ }
316
+ else {
317
+ r = await wo.delAllGfs(c.find())
318
+ }
319
+
320
+ //命中之檔案(含chunks)皆已刪除, 其餘不變
321
+ let names = await listNames(cl)
322
+ assert.strict.deepStrictEqual(names, ids.filter((v) => !c.del.includes(v)))
323
+ assert.strict.deepStrictEqual(r, { n: c.del.length, nDeleted: c.del.length, ok: 1 })
324
+ assert.strict.deepStrictEqual(await countChunks(cl), names.length)
325
+ assert.strict.deepStrictEqual(nType(evs, 'error'), 0)
326
+ assert.strict.deepStrictEqual(nType(evs, 'change'), 1)
327
+ assert.strict.deepStrictEqual(evs[0].mode, 'delAllGfs')
328
+
329
+ })
330
+
331
+ }
332
+
333
+ for (let fl of genFlips()) {
334
+
335
+ it(`should use the value that was checked and not read the given object again for delAllGfs with flipped ${fl.label}`, async function() {
336
+ this.timeout(180000)
337
+ await reseed(cl, wo)
338
+ let { find, stats } = fl.gen()
339
+ let r = await wo.delAllGfs(find)
340
+ assert.strict.deepStrictEqual(r, { n: 1, nDeleted: 1, ok: 1 })
341
+ assert.strict.deepStrictEqual(await listNames(cl), ['g2', 'g3', 'g4', 'g5', 'g6'])
342
+ assert.strict.deepStrictEqual(stats.map((v) => [v.desc, v.get]), [[1, 0]])
343
+ })
344
+
345
+ }
346
+
347
+ it('should delete by id for delGfs and not be affected by whitelist', async function() {
348
+ this.timeout(180000)
349
+ await reseed(cl, wo)
350
+ let r = await wo.delGfs([{ id: 'g1' }, { id: 'g3' }, { id: 'not-existed' }])
351
+ assert.strict.deepStrictEqual(r, [
352
+ { n: 1, nDeleted: 1, ok: 1 },
353
+ { n: 1, nDeleted: 1, ok: 1 },
354
+ { n: 0, nDeleted: 0, ok: 1 },
355
+ ])
356
+ assert.strict.deepStrictEqual(await listNames(cl), ['g2', 'g4', 'g5', 'g6'])
357
+ })
358
+
359
+ })
360
+
361
+
362
+ describe('find whitelist gfs: bad find does not reach mongodb', function() {
363
+ let cl = 'gfsnoreach'
364
+ let rejs = cases.filter((c) => c.expect === 'REJ')
365
+
366
+ it('should get 0 for count of MongoClient created by bad find of delAllGfs', async function() {
367
+ this.timeout(180000)
368
+ let woRaw = WOrm(genOpt(cl))
369
+ await reseed(cl, woRaw)
370
+ let { wo, count } = withSpyClient(() => WOrm(genOpt(cl)))
371
+ collect(wo)
372
+
373
+ //對照: 正常呼叫每次建立1個MongoClient
374
+ await wo.delAllGfs({ filename: 'not-existed' })
375
+ assert.strict.deepStrictEqual(count.n, 1)
376
+
377
+ //壞find一律不建立MongoClient
378
+ let n0 = count.n
379
+ let nRej = 0
380
+ for (let c of rejs) {
381
+ await wo.delAllGfs(c.find())
382
+ .catch(function() {
383
+ nRej++
384
+ })
385
+ }
386
+ assert.strict.deepStrictEqual(nRej, rejs.length)
387
+ assert.strict.deepStrictEqual(count.n - n0, 0)
388
+
389
+ //對照: 之後正常呼叫仍建立
390
+ await wo.delAllGfs({ filename: 'not-existed' })
391
+ assert.strict.deepStrictEqual(count.n - n0, 1)
392
+ assert.strict.deepStrictEqual(await listNames(cl), ids)
393
+
394
+ })
395
+
396
+ it('should get 0 for count of operations on server by bad find of delAllGfs', async function() {
397
+ this.timeout(180000)
398
+ let wo = WOrm(genOpt(cl))
399
+ await reseed(cl, wo)
400
+ collect(wo)
401
+ let db = rawClient.db(ctDb)
402
+ let countProf = async () => {
403
+ await delay(100)
404
+ return db.collection('system.profile').countDocuments({ ns: { $in: [`${ctDb}.${cl}.files`, `${ctDb}.${cl}.chunks`] } })
405
+ }
406
+
407
+ //profile level 2, 記錄伺服器收到之全部操作
408
+ await db.command({ profile: 2 })
409
+ try {
410
+
411
+ //對照: 正常呼叫伺服器會收到操作
412
+ let p0 = await countProf()
413
+ await wo.delAllGfs({ filename: 'not-existed' })
414
+ let p1 = await countProf()
415
+ assert.ok(p1 > p0, 'profiler should record the operation of normal delAllGfs')
416
+
417
+ //壞find不得送達伺服器
418
+ let nRej = 0
419
+ for (let c of rejs) {
420
+ await wo.delAllGfs(c.find())
421
+ .catch(function() {
422
+ nRej++
423
+ })
424
+ }
425
+ assert.strict.deepStrictEqual(nRej, rejs.length)
426
+ let p2 = await countProf()
427
+ assert.strict.deepStrictEqual(p2, p1)
428
+
429
+ //對照: 之後正常呼叫伺服器仍會收到操作
430
+ await wo.delAllGfs({ filename: 'not-existed' })
431
+ let p3 = await countProf()
432
+ assert.ok(p3 > p2, 'profiler should record the operation of normal delAllGfs after rejects')
433
+
434
+ }
435
+ finally {
436
+ await db.command({ profile: 0 })
437
+ }
438
+
439
+ assert.strict.deepStrictEqual(await listNames(cl), ids)
440
+ })
441
+
442
+ })
443
+
444
+
445
+ describe('find whitelist gfs: delGfs with id out of whitelist', function() {
446
+ let cl = 'gfsnul'
447
+
448
+ it('should get n 0 and ok 0 with invalid find for delGfs of ids out of whitelist, other ids not affected', async function() {
449
+ this.timeout(180000)
450
+ let wo = WOrm(genOpt(cl))
451
+ let evs = collect(wo)
452
+ await reseed(cl, wo)
453
+ let idNul = 'x\u0000y'
454
+ let r0 = await wo.insertGfs({ id: idNul, u8a: genU8a(5) })
455
+ assert.strict.deepStrictEqual(r0.nInserted, 1)
456
+ evs.length = 0
457
+
458
+ //delGfs, 條件未通過白名單檢核(id含NUL或孤立代理字元)者尚未查找, n為0、ok為0, 逐筆失敗不中斷同批其他筆, 不論該id是否存在
459
+ let msgNul = 'invalid find: string contains NUL at [find.filename]'
460
+ let msgLone = 'invalid find: string contains lone surrogate at [find.filename]'
461
+ let r = await wo.delGfs([{ id: idNul }, { id: 'absent\u0000id' }, { id: '\uD800' }, { id: 'ok\uDC00' }, { id: 'g1' }])
462
+ assert.strict.deepStrictEqual(r, [
463
+ { n: 0, nDeleted: 0, ok: 0, err: msgNul },
464
+ { n: 0, nDeleted: 0, ok: 0, err: msgNul },
465
+ { n: 0, nDeleted: 0, ok: 0, err: msgLone },
466
+ { n: 0, nDeleted: 0, ok: 0, err: msgLone },
467
+ { n: 1, nDeleted: 1, ok: 1 },
468
+ ])
469
+
470
+ //每個失敗筆各發出一次error, 整批一次change
471
+ assert.strict.deepStrictEqual(nType(evs, 'error'), 4)
472
+ assert.strict.deepStrictEqual(evs.filter((v) => v.type === 'error').map((v) => [v.mode, v.err]), [
473
+ ['delGfs', msgNul],
474
+ ['delGfs', msgNul],
475
+ ['delGfs', msgLone],
476
+ ['delGfs', msgLone],
477
+ ])
478
+ assert.strict.deepStrictEqual(nType(evs, 'change'), 1)
479
+ assert.strict.deepStrictEqual(await listNames(cl), ['g2', 'g3', 'g4', 'g5', 'g6', idNul].sort())
480
+
481
+ //仍可用delAllGfs()全刪
482
+ let rAll = await wo.delAllGfs()
483
+ assert.strict.deepStrictEqual(rAll, { n: 6, nDeleted: 6, ok: 1 })
484
+ assert.strict.deepStrictEqual(await listNames(cl), [])
485
+ assert.strict.deepStrictEqual(await countChunks(cl), 0)
486
+
487
+ })
488
+
489
+ })