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,540 @@
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
+ //條件(find)白名單之接線測試, 規則見CLAUDE_rulebook.md R-FIND-01
10
+ //契約案例表(wsemi/src/_cvOrmFindCases.mjs)逐案對公開select與delAll執行, 種子與快照一律由驅動直接操作, 不經受測套件
11
+
12
+
13
+ let { seed, cases } = cvOrmFindCases
14
+
15
+
16
+ let ctName = 'worm-test-mongodb-find-whitelist'
17
+ let ctDb = 'worm'
18
+ let ctPort = null //容器對外埠, 由startContainer動態取得
19
+ let rawClient = null
20
+
21
+
22
+ let allIds = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6']
23
+
24
+
25
+ //genOpt
26
+ let genOpt = (cl) => {
27
+ return {
28
+ url: genUrl(ctPort),
29
+ db: ctDb,
30
+ cl,
31
+ }
32
+ }
33
+
34
+ //getCl, 以驅動直接取資料表物件
35
+ let getCl = (cl) => {
36
+ return rawClient.db(ctDb).collection(cl)
37
+ }
38
+
39
+ //reseed, 以驅動重建6列種子(a1~a6)
40
+ let reseed = async (cl) => {
41
+ let c = getCl(cl)
42
+ await c.deleteMany({})
43
+ await c.insertMany(seed())
44
+ }
45
+
46
+ //snap, 以驅動取資料表快照(去除_id並依id排序), 用於驗證資料不變
47
+ let snap = async (cl) => {
48
+ return getCl(cl).find({}, { projection: { _id: 0 } }).sort({ id: 1 }).toArray()
49
+ }
50
+
51
+ //idsOf, 取id陣列並升冪排序
52
+ let idsOf = (rs) => {
53
+ return rs.map((v) => v.id).sort()
54
+ }
55
+
56
+ //collect, 蒐集事件(依發生順序)
57
+ let collect = (wo) => {
58
+ let evs = []
59
+ wo.on('error', function(mode, data, err) {
60
+ evs.push({ type: 'error', mode, data, err })
61
+ })
62
+ wo.on('change', function(mode, data, res) {
63
+ evs.push({ type: 'change', mode, data, res })
64
+ })
65
+ return evs
66
+ }
67
+
68
+ //nType, 事件陣列內指定類型之筆數
69
+ let nType = (evs, type) => {
70
+ return evs.filter((v) => v.type === type).length
71
+ }
72
+
73
+ //getMsg, 取共用檢核對該find之錯誤訊息(本套件須原樣reject該訊息)
74
+ let getMsg = (find) => {
75
+ let msg = null
76
+ try {
77
+ cvOrmFind(find)
78
+ }
79
+ catch (err) {
80
+ msg = err.message
81
+ }
82
+ return msg
83
+ }
84
+
85
+ //withSpyClient, 於建立受測物件期間以計數替身取代MongoClient(受測套件於建立時取用MongoClient), 完成即還原
86
+ let withSpyClient = (fn) => {
87
+ let Orig = mongodb.MongoClient
88
+ let desc = Object.getOwnPropertyDescriptor(mongodb, 'MongoClient')
89
+ let count = { n: 0 }
90
+ class Spy extends Orig {
91
+ constructor(...args) {
92
+ super(...args)
93
+ count.n++
94
+ }
95
+ }
96
+ Object.defineProperty(mongodb, 'MongoClient', { configurable: true, enumerable: true, get: () => Spy })
97
+ let wo = null
98
+ try {
99
+ wo = fn()
100
+ }
101
+ finally {
102
+ Object.defineProperty(mongodb, 'MongoClient', desc)
103
+ }
104
+ return { wo, count }
105
+ }
106
+
107
+ //genFlipSpy, 產生包裝target之Proxy, 其key欄僅第一次被讀取時為v1, 其後(含get陷阱)皆為v2(用於驗證檢查之值即使用之值),
108
+ //回傳{ proxy, stat }, stat記錄key被屬性描述子讀取(desc)與被get讀取(get)之次數: 檢核須只以描述子讀取一次, 其後不得再讀取原物件
109
+ let genFlipSpy = (target, key, v1, v2) => {
110
+ let stat = { desc: 0, get: 0 }
111
+ let n = 0
112
+ let proxy = new Proxy(target, {
113
+ get(t, k, r) {
114
+ if (k === key) {
115
+ stat.get++
116
+ n++
117
+ return n === 1 ? v1 : v2
118
+ }
119
+ return Reflect.get(t, k, r)
120
+ },
121
+ getOwnPropertyDescriptor(t, k) {
122
+ let d = Reflect.getOwnPropertyDescriptor(t, k)
123
+ if (k === key) {
124
+ stat.desc++
125
+ n++
126
+ d.value = n === 1 ? v1 : v2
127
+ }
128
+ return d
129
+ },
130
+ })
131
+ return { proxy, stat }
132
+ }
133
+
134
+ //genFlips, 翻轉案例: 條件內各層(頂層欄、運算子物件、$in陣列元素、$and陣列元素、$or元素內欄位、Date值)各有一處被翻轉, 檢核後之複本須為第一次讀取之值(皆命中a1)
135
+ let genFlips = () => {
136
+ let D1 = new Date(Date.UTC(2026, 0, 1))
137
+ let D2 = new Date(Date.UTC(2026, 1, 1))
138
+ let r = []
139
+ let add = (label, fn) => {
140
+ r.push({ label, gen: fn })
141
+ }
142
+ add('top level field', () => {
143
+ let s = genFlipSpy({ id: 'a1' }, 'id', 'a1', 'a2')
144
+ return { find: s.proxy, stats: [s.stat] }
145
+ })
146
+ add('operator object $in', () => {
147
+ let s = genFlipSpy({ $in: ['a1'] }, '$in', ['a1'], ['a2'])
148
+ return { find: { id: s.proxy }, stats: [s.stat] }
149
+ })
150
+ add('element of $in array', () => {
151
+ let s = genFlipSpy(['a1'], '0', 'a1', 'a2')
152
+ return { find: { id: { $in: s.proxy } }, stats: [s.stat] }
153
+ })
154
+ add('element of $and array', () => {
155
+ let s = genFlipSpy([{ id: 'a1' }], '0', { id: 'a1' }, { id: 'a2' })
156
+ return { find: { $and: s.proxy }, stats: [s.stat] }
157
+ })
158
+ add('field in element of $or array', () => {
159
+ let s = genFlipSpy({ id: 'a1' }, 'id', 'a1', 'a2')
160
+ return { find: { $or: [s.proxy] }, stats: [s.stat] }
161
+ })
162
+ add('Date value', () => {
163
+ let s = genFlipSpy({ d: new Date(0) }, 'd', D1, D2)
164
+ return { find: s.proxy, stats: [s.stat] }
165
+ })
166
+ return r
167
+ }
168
+
169
+
170
+ before(async function() {
171
+ this.timeout(600000) //含拉取映像與服務啟動
172
+ ctPort = await startContainer(ctName)
173
+ rawClient = new mongodb.MongoClient(genUrl(ctPort))
174
+ await rawClient.connect()
175
+ })
176
+
177
+ after(async function() {
178
+ this.timeout(120000)
179
+ if (rawClient) {
180
+ await rawClient.close()
181
+ }
182
+ await stopContainer(ctName)
183
+ })
184
+
185
+
186
+ describe('find whitelist: cases', function() {
187
+ let cl = 'cases'
188
+ let wo = null
189
+ let evs = null
190
+
191
+ before(function() {
192
+ wo = WOrm(genOpt(cl))
193
+ evs = collect(wo)
194
+ })
195
+
196
+ it('should get true for count of contract cases not less than 241', function() {
197
+ assert.ok(cases.length >= 241, `contract cases are ${cases.length}`)
198
+ })
199
+
200
+ for (let c of cases) {
201
+
202
+ if (c.expect === 'REJ') {
203
+
204
+ for (let mode of ['select', 'delAll']) {
205
+
206
+ it(`should get "invalid find" and keep data for ${mode} ${c.id} ${c.label}`, async function() {
207
+ this.timeout(180000)
208
+ await reseed(cl)
209
+ let before = await snap(cl)
210
+ let msgExp = getMsg(c.find())
211
+ evs.length = 0
212
+
213
+ //call
214
+ let err = null
215
+ let nErrAtReject = -1
216
+ let nChgAtReject = -1
217
+ await wo[mode](c.find())
218
+ .catch(function(e) {
219
+ err = e
220
+ nErrAtReject = nType(evs, 'error')
221
+ nChgAtReject = nType(evs, 'change')
222
+ })
223
+
224
+ //reject, 訊息原樣(不包裝)
225
+ assert.ok(err instanceof Error, `${mode} should reject`)
226
+ assert.strict.deepStrictEqual(typeof msgExp, 'string')
227
+ assert.strict.deepStrictEqual(msgExp.indexOf('invalid find: '), 0)
228
+ assert.strict.deepStrictEqual(err.message, msgExp)
229
+
230
+ //error事件須於reject之前恰發出一次, change不發
231
+ assert.strict.deepStrictEqual(nErrAtReject, 1)
232
+ assert.strict.deepStrictEqual(nChgAtReject, 0)
233
+ await delay(30)
234
+ assert.strict.deepStrictEqual(nType(evs, 'error'), 1)
235
+ assert.strict.deepStrictEqual(nType(evs, 'change'), 0)
236
+ assert.strict.deepStrictEqual(evs[0].mode, mode)
237
+ assert.strict.deepStrictEqual(evs[0].data, null)
238
+ assert.strict.deepStrictEqual(evs[0].err, msgExp)
239
+
240
+ //資料不變
241
+ assert.strict.deepStrictEqual(await snap(cl), before)
242
+
243
+ })
244
+
245
+ }
246
+
247
+ }
248
+ else {
249
+
250
+ it(`should get ${JSON.stringify(c.ids.mongo)} for select ${c.id} ${c.label}`, async function() {
251
+ this.timeout(180000)
252
+ await reseed(cl)
253
+ evs.length = 0
254
+ let rs = await wo.select(c.find())
255
+ assert.strict.deepStrictEqual(idsOf(rs), c.ids.mongo)
256
+ assert.strict.deepStrictEqual(rs.every((v) => !('_id' in v)), true)
257
+ assert.strict.deepStrictEqual(evs.length, 0)
258
+ })
259
+
260
+ it(`should delete the same ids as select for delAll ${c.id} ${c.label}`, async function() {
261
+ this.timeout(180000)
262
+ await reseed(cl)
263
+ let selected = idsOf(await wo.select(c.find()))
264
+ assert.strict.deepStrictEqual(selected, c.ids.mongo)
265
+ evs.length = 0
266
+
267
+ //delAll
268
+ let r = await wo.delAll(c.find())
269
+ let remain = idsOf(await snap(cl))
270
+ let deleted = allIds.filter((v) => !remain.includes(v))
271
+
272
+ //刪除集合=select命中集合, n與nDeleted皆為實際刪除筆數
273
+ assert.strict.deepStrictEqual(deleted, selected)
274
+ assert.strict.deepStrictEqual(r, { n: selected.length, nDeleted: selected.length, ok: 1 })
275
+ assert.strict.deepStrictEqual(evs.length, 1)
276
+ assert.strict.deepStrictEqual(evs[0].type, 'change')
277
+ assert.strict.deepStrictEqual(evs[0].mode, 'delAll')
278
+ })
279
+
280
+ }
281
+
282
+ }
283
+
284
+ })
285
+
286
+
287
+ describe('find whitelist: top level', function() {
288
+ let cl = 'top'
289
+ let wo = null
290
+ let evs = null
291
+
292
+ before(function() {
293
+ wo = WOrm(genOpt(cl))
294
+ evs = collect(wo)
295
+ })
296
+
297
+ it('should get all 6 for select() without find', async function() {
298
+ await reseed(cl)
299
+ let rs = await wo.select()
300
+ assert.strict.deepStrictEqual(idsOf(rs), allIds)
301
+ })
302
+
303
+ it('should get all 6 deleted for delAll() without find', async function() {
304
+ await reseed(cl)
305
+ let r = await wo.delAll()
306
+ assert.strict.deepStrictEqual(r, { n: 6, nDeleted: 6, ok: 1 })
307
+ assert.strict.deepStrictEqual(await snap(cl), [])
308
+ })
309
+
310
+ it('should get all 6 deleted for delAll({}) and delAll(undefined)', async function() {
311
+ await reseed(cl)
312
+ let r1 = await wo.delAll({})
313
+ assert.strict.deepStrictEqual(r1, { n: 6, nDeleted: 6, ok: 1 })
314
+ await reseed(cl)
315
+ let r2 = await wo.delAll(undefined)
316
+ assert.strict.deepStrictEqual(r2, { n: 6, nDeleted: 6, ok: 1 })
317
+ await reseed(cl)
318
+ let r3 = await wo.delAll(Object.create(null))
319
+ assert.strict.deepStrictEqual(r3, { n: 6, nDeleted: 6, ok: 1 })
320
+ })
321
+
322
+ it('should get 0 deleted for delAll with no hit', async function() {
323
+ await reseed(cl)
324
+ evs.length = 0
325
+ let r = await wo.delAll({ id: 'not-existed' })
326
+ assert.strict.deepStrictEqual(r, { n: 0, nDeleted: 0, ok: 1 })
327
+ assert.strict.deepStrictEqual(nType(evs, 'error'), 0)
328
+ assert.strict.deepStrictEqual((await snap(cl)).length, 6)
329
+ })
330
+
331
+ it('should get row without _id and same fields as seeded for select', async function() {
332
+ await reseed(cl)
333
+ let rs = await wo.select({ id: 'a1' })
334
+ assert.strict.deepStrictEqual(rs, [seed()[0]])
335
+ assert.strict.deepStrictEqual(Object.keys(rs[0]).includes('_id'), false)
336
+ let rsAll = await wo.select({})
337
+ assert.strict.deepStrictEqual(rsAll.some((v) => '_id' in v), false)
338
+ })
339
+
340
+ it('should reject $regex and keep working after reject', async function() {
341
+ await reseed(cl)
342
+ let msg = null
343
+ await wo.select({ name: { $regex: 'PeT', $options: 'i' } })
344
+ .catch(function(err) {
345
+ msg = err.toString()
346
+ })
347
+ assert.strict.deepStrictEqual(msg, 'Error: invalid find: unsupported operator [$regex] at [find.name.$regex]')
348
+ let rs = await wo.select({ name: 'Peter' })
349
+ assert.strict.deepStrictEqual(idsOf(rs), ['a1'])
350
+ })
351
+
352
+ for (let fl of genFlips()) {
353
+
354
+ it(`should use the value that was checked and not read the given object again for select with flipped ${fl.label}`, async function() {
355
+ await reseed(cl)
356
+ let { find, stats } = fl.gen()
357
+ let rs = await wo.select(find)
358
+ assert.strict.deepStrictEqual(idsOf(rs), ['a1'])
359
+ assert.strict.deepStrictEqual(stats.map((v) => [v.desc, v.get]), [[1, 0]])
360
+ })
361
+
362
+ it(`should use the value that was checked and not read the given object again for delAll with flipped ${fl.label}`, async function() {
363
+ await reseed(cl)
364
+ let { find, stats } = fl.gen()
365
+ let r = await wo.delAll(find)
366
+ assert.strict.deepStrictEqual(r, { n: 1, nDeleted: 1, ok: 1 })
367
+ assert.strict.deepStrictEqual(idsOf(await snap(cl)), ['a2', 'a3', 'a4', 'a5', 'a6'])
368
+ assert.strict.deepStrictEqual(stats.map((v) => [v.desc, v.get]), [[1, 0]])
369
+ })
370
+
371
+ }
372
+
373
+ it('should reject BSON reserved field name _bsontype without matching or deleting any data', async function() {
374
+ await reseed(cl)
375
+ let finds = [
376
+ () => ({ _bsontype: 'x' }),
377
+ () => ({ _bsontype: 'ObjectId' }),
378
+ () => ({ _bsontype: 1 }),
379
+ () => ({ $and: [{ _bsontype: 'x' }] }),
380
+ () => ({ _bsontype: { $eq: 'x' } }),
381
+ ]
382
+ for (let fn of finds) {
383
+
384
+ //select, 白名單收此欄名(符合欄名文法), 但BSON以_bsontype辨識型別而無法作為一般欄位送出, 須reject或無命中, 不得命中任何數據
385
+ let rs = null
386
+ await wo.select(fn())
387
+ .then(function(v) {
388
+ rs = v
389
+ })
390
+ .catch(function() {
391
+ rs = 'rejected'
392
+ })
393
+ assert.ok(rs === 'rejected' || (Array.isArray(rs) && rs.length === 0))
394
+
395
+ //delAll
396
+ let r = null
397
+ await wo.delAll(fn())
398
+ .then(function(v) {
399
+ r = v
400
+ })
401
+ .catch(function() {
402
+ r = 'rejected'
403
+ })
404
+ assert.ok(r === 'rejected' || r.nDeleted === 0)
405
+
406
+ }
407
+ assert.strict.deepStrictEqual((await snap(cl)).length, 6)
408
+ })
409
+
410
+ it('should not change the find object given by caller', async function() {
411
+ await reseed(cl)
412
+ let find = { $or: [{ g: 'A' }, { age: { $in: [50] } }], name: { $ne: 'zz' } }
413
+ let txt = JSON.stringify(find)
414
+ let rs = await wo.select(find)
415
+ assert.strict.deepStrictEqual(idsOf(rs), ['a1', 'a2', 'a5'])
416
+ await wo.delAll(find)
417
+ assert.strict.deepStrictEqual(JSON.stringify(find), txt)
418
+ assert.strict.deepStrictEqual(Object.getPrototypeOf(find), Object.prototype)
419
+ })
420
+
421
+ })
422
+
423
+
424
+ describe('find whitelist: bad find does not reach mongodb', function() {
425
+ let cl = 'noreach'
426
+ let rejs = cases.filter((c) => c.expect === 'REJ')
427
+
428
+ it('should get 0 for count of MongoClient created by bad find of select and delAll', async function() {
429
+ this.timeout(180000)
430
+ await reseed(cl)
431
+ let { wo, count } = withSpyClient(() => WOrm(genOpt(cl)))
432
+ collect(wo)
433
+
434
+ //對照: 正常呼叫每次建立1個MongoClient
435
+ await wo.select({})
436
+ assert.strict.deepStrictEqual(count.n, 1)
437
+ await wo.delAll({ id: 'not-existed' })
438
+ assert.strict.deepStrictEqual(count.n, 2)
439
+
440
+ //壞find一律不建立MongoClient
441
+ let n0 = count.n
442
+ let nRej = 0
443
+ for (let c of rejs) {
444
+ for (let mode of ['select', 'delAll']) {
445
+ await wo[mode](c.find())
446
+ .catch(function() {
447
+ nRej++
448
+ })
449
+ }
450
+ }
451
+ assert.strict.deepStrictEqual(nRej, rejs.length * 2)
452
+ assert.strict.deepStrictEqual(count.n - n0, 0)
453
+
454
+ //對照: 之後正常呼叫仍建立
455
+ await wo.select({ id: 'a1' })
456
+ assert.strict.deepStrictEqual(count.n - n0, 1)
457
+ assert.strict.deepStrictEqual((await snap(cl)).length, 6)
458
+
459
+ })
460
+
461
+ it('should get 0 for count of operations on server by bad find of select and delAll', async function() {
462
+ this.timeout(180000)
463
+ await reseed(cl)
464
+ let db = rawClient.db(ctDb)
465
+ let ns = `${ctDb}.${cl}`
466
+ let countProf = async () => {
467
+ await delay(100)
468
+ return db.collection('system.profile').countDocuments({ ns })
469
+ }
470
+
471
+ //profile level 2, 記錄伺服器收到之全部操作
472
+ await db.command({ profile: 2 })
473
+ try {
474
+
475
+ let wo = WOrm(genOpt(cl))
476
+ collect(wo)
477
+
478
+ //對照: 正常呼叫伺服器會收到操作(select與delAll各自證明)
479
+ let p00 = await countProf()
480
+ await wo.select({ id: 'a1' })
481
+ let p0 = await countProf()
482
+ assert.ok(p0 > p00, 'profiler should record the operation of normal select')
483
+ await wo.delAll({ id: 'not-existed' })
484
+ let p1 = await countProf()
485
+ assert.ok(p1 > p0, 'profiler should record the operation of normal delAll')
486
+
487
+ //壞find不得送達伺服器
488
+ let nRej = 0
489
+ for (let c of rejs) {
490
+ for (let mode of ['select', 'delAll']) {
491
+ await wo[mode](c.find())
492
+ .catch(function() {
493
+ nRej++
494
+ })
495
+ }
496
+ }
497
+ assert.strict.deepStrictEqual(nRej, rejs.length * 2)
498
+ let p2 = await countProf()
499
+ assert.strict.deepStrictEqual(p2, p1)
500
+
501
+ //對照: 之後正常呼叫伺服器仍會收到操作
502
+ await wo.select({ id: 'a1' })
503
+ let p3 = await countProf()
504
+ assert.ok(p3 > p2, 'profiler should record the operation of normal select after rejects')
505
+
506
+ }
507
+ finally {
508
+ await db.command({ profile: 0 })
509
+ }
510
+
511
+ assert.strict.deepStrictEqual((await snap(cl)).length, 6)
512
+ })
513
+
514
+ it('should reject $where string and function at once without running them', async function() {
515
+ this.timeout(180000)
516
+ await reseed(cl)
517
+ let wo = WOrm(genOpt(cl))
518
+ let evs = collect(wo)
519
+ let fnWhere = () => true
520
+ for (let mode of ['select', 'delAll']) {
521
+ for (let find of [{ $where: 'sleep(3000) || true' }, { $where: fnWhere }, { id: 'a1', $where: 'true' }]) {
522
+ evs.length = 0
523
+ let t0 = Date.now()
524
+ let err = null
525
+ await wo[mode](find)
526
+ .catch(function(e) {
527
+ err = e
528
+ })
529
+ let dt = Date.now() - t0
530
+ assert.ok(err instanceof Error)
531
+ assert.strict.deepStrictEqual(err.message.indexOf('invalid find: unsupported operator [$where]'), 0)
532
+ assert.ok(dt < 2000, `${mode} should reject $where at once, but spent ${dt}ms`)
533
+ assert.strict.deepStrictEqual(nType(evs, 'error'), 1)
534
+ assert.strict.deepStrictEqual(nType(evs, 'change'), 0)
535
+ }
536
+ }
537
+ assert.strict.deepStrictEqual((await snap(cl)).length, 6)
538
+ })
539
+
540
+ })