w-orm-mongodb 1.1.42 → 1.1.43

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,290 @@
1
+ import assert from 'assert'
2
+ import mongodb from 'mongodb'
3
+ import WOrm from '../src/WOrmMongodb.mjs'
4
+ import { genUrlRs, startContainerRs, stopContainer } from './lib/api-setup.mjs'
5
+
6
+
7
+ //insertBulk之全有全無有兩條實作路徑, 依部署而異:
8
+ //具replica set或分片叢集者以交易包覆, standalone無交易可用而以補償動作達成
9
+ //本檔專測交易路徑, 故另起replica set容器; 補償路徑見api-basic.test.mjs之insertBulk
10
+
11
+
12
+ let ctName = 'worm-test-mongodb-rs'
13
+ let ctDb = 'worm'
14
+ let ctPort = null //容器對外埠, 由startContainerRs動態取得
15
+
16
+
17
+ //genOpt
18
+ let genOpt = (cl) => {
19
+ return {
20
+ url: genUrlRs(ctPort),
21
+ db: ctDb,
22
+ cl,
23
+ }
24
+ }
25
+
26
+ //genKeys
27
+ let genKeys = (v) => {
28
+ return Object.keys(v).sort().join(',')
29
+ }
30
+
31
+ //getHello, 直接由驅動取拓樸資訊, 用於確認本容器確為replica set
32
+ let getHello = async () => {
33
+ let client = new mongodb.MongoClient(genUrlRs(ctPort))
34
+ let hello = null
35
+ try {
36
+ hello = await client.db(ctDb).admin().command({ hello: 1 })
37
+ }
38
+ finally {
39
+ await client.close()
40
+ }
41
+ return hello
42
+ }
43
+
44
+
45
+ before(async function() {
46
+ this.timeout(600000) //含拉取映像、服務啟動與選舉
47
+ ctPort = await startContainerRs(ctName)
48
+ })
49
+
50
+ after(async function() {
51
+ this.timeout(120000)
52
+ await stopContainer(ctName)
53
+ })
54
+
55
+
56
+ describe('insertBulk by transaction', function() {
57
+ let rt = null
58
+ let vans = {}
59
+ let vget = {}
60
+
61
+ before(async function() {
62
+ this.timeout(300000)
63
+
64
+ //確認本容器確為replica set, 否則以下即非在測交易路徑
65
+ //套件內判定交易可用與否之依據即為hello之setName與msg, 故此處成立即表示套件必走交易路徑
66
+ let hello = await getHello()
67
+ vget[1] = {
68
+ isRs: typeof hello.setName === 'string' && hello.setName.length > 0,
69
+ isWritablePrimary: hello.isWritablePrimary === true,
70
+ }
71
+
72
+ let cl = 'bulktx'
73
+ let wo = WOrm(genOpt(cl))
74
+ await wo.delAll()
75
+
76
+ //全新3筆, nInserted須等於n
77
+ vget[2] = await wo.insertBulk([
78
+ { id: 't1', name: 'peter' },
79
+ { id: 't2', name: 'rosemary' },
80
+ { id: 't3', name: 'kettle' },
81
+ ])
82
+ vget[3] = (await wo.select()).length
83
+
84
+ //撞既有主鍵須整批reject且回滾
85
+ rt = null
86
+ let nBefore = (await wo.select()).length
87
+ await wo.insertBulk([
88
+ { id: 't4', name: 'new1' },
89
+ { id: 't2', name: 'dup' },
90
+ { id: 't5', name: 'new2' },
91
+ ])
92
+ .then(function(msg) {
93
+ rt = `不應resolve: ${JSON.stringify(msg)}`
94
+ })
95
+ .catch(function(msg) {
96
+ rt = msg.toString()
97
+ })
98
+ vget[4] = rt.indexOf('E11000') >= 0 || rt.indexOf('duplicate key') >= 0
99
+
100
+ //回滾後資料表不得有任何新增
101
+ vget[5] = (await wo.select()).length - nBefore
102
+ vget[6] = [
103
+ await wo.selectByPk('t4'),
104
+ await wo.selectByPk('t5'),
105
+ ]
106
+
107
+ //撞既有主鍵者本身不得被改動
108
+ vget[7] = (await wo.selectByPk('t2')).name
109
+
110
+ //同批含重複主鍵須整批reject且回滾
111
+ rt = null
112
+ nBefore = (await wo.select()).length
113
+ await wo.insertBulk([
114
+ { id: 't6', name: 'a' },
115
+ { id: 't6', name: 'b' },
116
+ { id: 't7', name: 'c' },
117
+ ])
118
+ .then(function(msg) {
119
+ rt = `不應resolve: ${JSON.stringify(msg)}`
120
+ })
121
+ .catch(function(msg) {
122
+ rt = msg.toString()
123
+ })
124
+ vget[8] = rt.indexOf('E11000') >= 0 || rt.indexOf('duplicate key') >= 0
125
+ vget[9] = (await wo.select()).length - nBefore
126
+ vget[10] = [
127
+ await wo.selectByPk('t6'),
128
+ await wo.selectByPk('t7'),
129
+ ]
130
+
131
+ //較大批次亦須全有全無, 用於驗證分批送出時之回滾
132
+ rt = null
133
+ await wo.delAll()
134
+ let rs = Array.from({ length: 200 }, (v, k) => {
135
+ return { id: `m${k}`, name: `n${k}` }
136
+ })
137
+ vget[11] = await wo.insertBulk(rs)
138
+ vget[12] = (await wo.select()).length
139
+
140
+ //末筆撞既有主鍵, 前199筆皆不得殘留
141
+ rt = null
142
+ await wo.delAll()
143
+ await wo.insert({ id: 'm199', name: 'pre' })
144
+ nBefore = (await wo.select()).length
145
+ await wo.insertBulk(rs)
146
+ .then(function(msg) {
147
+ rt = `不應resolve: ${JSON.stringify(msg)}`
148
+ })
149
+ .catch(function(msg) {
150
+ rt = msg.toString()
151
+ })
152
+ vget[13] = rt.indexOf('E11000') >= 0 || rt.indexOf('duplicate key') >= 0
153
+ vget[14] = (await wo.select()).length - nBefore
154
+
155
+ //輸入無效與單一物件
156
+ let woS = WOrm(genOpt('bulktxmisc'))
157
+ await woS.delAll()
158
+ vget[15] = await woS.insertBulk(null)
159
+ vget[16] = await woS.insertBulk({ id: 's1', name: 'single' })
160
+ vget[17] = [genKeys(vget[15]), genKeys(vget[16])]
161
+
162
+ //事件
163
+ let woEv = WOrm(genOpt('bulktxevent'))
164
+ await woEv.delAll()
165
+ let evs = []
166
+ woEv.on('change', function(mode) {
167
+ evs.push(`change:${mode}`)
168
+ })
169
+ woEv.on('error', function(mode) {
170
+ evs.push(`error:${mode}`)
171
+ })
172
+ await woEv.insertBulk({ id: 'e1', name: 'a' })
173
+ await woEv.insertBulk({ id: 'e1', name: 'b' }).catch(() => {})
174
+ vget[18] = evs
175
+
176
+ //其餘函數於replica set下行為須與standalone相同
177
+ let woO = WOrm(genOpt('bulktxother'))
178
+ await woO.delAll()
179
+ vget[19] = await woO.insert([{ id: 'o1' }, { id: 'o1' }, { id: 'o2' }])
180
+ vget[20] = await woO.save({ id: 'o1', name: 'x' })
181
+ vget[21] = await woO.del({ id: 'o2' })
182
+
183
+ })
184
+
185
+ vans[1] = { isRs: true, isWritablePrimary: true }
186
+ it(`should get ${JSON.stringify(vans[1])} for topology being replica set`, async function() {
187
+ assert.strict.deepStrictEqual(vget[1], vans[1])
188
+ })
189
+
190
+ vans[2] = { n: 3, nInserted: 3, ok: 1 }
191
+ it(`should get ${JSON.stringify(vans[2])} for insertBulk 3 new records`, async function() {
192
+ assert.strict.deepStrictEqual(vget[2], vans[2])
193
+ })
194
+
195
+ vans[3] = 3
196
+ it(`should get ${JSON.stringify(vans[3])} for records after insertBulk`, async function() {
197
+ assert.strict.deepStrictEqual(vget[3], vans[3])
198
+ })
199
+
200
+ vans[4] = true
201
+ it(`should get ${JSON.stringify(vans[4])} for reject of insertBulk with existed id`, async function() {
202
+ assert.strict.deepStrictEqual(vget[4], vans[4])
203
+ })
204
+
205
+ vans[5] = 0
206
+ it(`should get ${JSON.stringify(vans[5])} for rollback after rejected insertBulk`, async function() {
207
+ assert.strict.deepStrictEqual(vget[5], vans[5])
208
+ })
209
+
210
+ vans[6] = [null, null]
211
+ it(`should get ${JSON.stringify(vans[6])} for not writing any record in rejected insertBulk`, async function() {
212
+ assert.strict.deepStrictEqual(vget[6], vans[6])
213
+ })
214
+
215
+ vans[7] = 'rosemary'
216
+ it(`should get ${JSON.stringify(vans[7])} for not modifying existed record by rejected insertBulk`, async function() {
217
+ assert.strict.deepStrictEqual(vget[7], vans[7])
218
+ })
219
+
220
+ vans[8] = true
221
+ it(`should get ${JSON.stringify(vans[8])} for reject of insertBulk with duplicated id in same batch`, async function() {
222
+ assert.strict.deepStrictEqual(vget[8], vans[8])
223
+ })
224
+
225
+ vans[9] = 0
226
+ it(`should get ${JSON.stringify(vans[9])} for rollback after insertBulk with duplicated id`, async function() {
227
+ assert.strict.deepStrictEqual(vget[9], vans[9])
228
+ })
229
+
230
+ vans[10] = [null, null]
231
+ it(`should get ${JSON.stringify(vans[10])} for not writing any record by duplicated id in same batch`, async function() {
232
+ assert.strict.deepStrictEqual(vget[10], vans[10])
233
+ })
234
+
235
+ vans[11] = { n: 200, nInserted: 200, ok: 1 }
236
+ it(`should get ${JSON.stringify(vans[11])} for insertBulk 200 records`, async function() {
237
+ assert.strict.deepStrictEqual(vget[11], vans[11])
238
+ })
239
+
240
+ vans[12] = 200
241
+ it(`should get ${JSON.stringify(vans[12])} for records after insertBulk 200 records`, async function() {
242
+ assert.strict.deepStrictEqual(vget[12], vans[12])
243
+ })
244
+
245
+ vans[13] = true
246
+ it(`should get ${JSON.stringify(vans[13])} for reject of insertBulk 200 records with last one conflicted`, async function() {
247
+ assert.strict.deepStrictEqual(vget[13], vans[13])
248
+ })
249
+
250
+ vans[14] = 0
251
+ it(`should get ${JSON.stringify(vans[14])} for rollback of 200 records batch`, async function() {
252
+ assert.strict.deepStrictEqual(vget[14], vans[14])
253
+ })
254
+
255
+ vans[15] = { n: 0, nInserted: 0, ok: 1 }
256
+ it(`should get ${JSON.stringify(vans[15])} for insertBulk with invalid data`, async function() {
257
+ assert.strict.deepStrictEqual(vget[15], vans[15])
258
+ })
259
+
260
+ vans[16] = { n: 1, nInserted: 1, ok: 1 }
261
+ it(`should get ${JSON.stringify(vans[16])} for insertBulk with single object`, async function() {
262
+ assert.strict.deepStrictEqual(vget[16], vans[16])
263
+ })
264
+
265
+ vans[17] = ['n,nInserted,ok', 'n,nInserted,ok']
266
+ it(`should get ${JSON.stringify(vans[17])} for fixed key set of insertBulk`, async function() {
267
+ assert.strict.deepStrictEqual(vget[17], vans[17])
268
+ })
269
+
270
+ vans[18] = ['change:insertBulk', 'error:insertBulk']
271
+ it(`should get ${JSON.stringify(vans[18])} for events of insertBulk`, async function() {
272
+ assert.strict.deepStrictEqual(vget[18], vans[18])
273
+ })
274
+
275
+ vans[19] = { n: 3, nInserted: 2, ok: 1 }
276
+ it(`should get ${JSON.stringify(vans[19])} for insert on replica set`, async function() {
277
+ assert.strict.deepStrictEqual(vget[19], vans[19])
278
+ })
279
+
280
+ vans[20] = [{ n: 1, nInserted: 0, nModified: 1, ok: 1 }]
281
+ it(`should get ${JSON.stringify(vans[20])} for save on replica set`, async function() {
282
+ assert.strict.deepStrictEqual(vget[20], vans[20])
283
+ })
284
+
285
+ vans[21] = [{ n: 1, nDeleted: 1, ok: 1 }]
286
+ it(`should get ${JSON.stringify(vans[21])} for del on replica set`, async function() {
287
+ assert.strict.deepStrictEqual(vget[21], vans[21])
288
+ })
289
+
290
+ })
@@ -45,6 +45,13 @@ let genUrl = (port) => {
45
45
  return `mongodb://${ctUser}:${ctPassword}@127.0.0.1:${port}`
46
46
  }
47
47
 
48
+ //genUrlRs, replica set容器之連線字串
49
+ //directConnection=true係因容器內之replica set成員位址為127.0.0.1:27017, 由宿主機依該位址連不到,
50
+ //故不可令驅動走成員探索, 須直連本次映射之對外埠
51
+ let genUrlRs = (port) => {
52
+ return `mongodb://127.0.0.1:${port}/?directConnection=true`
53
+ }
54
+
48
55
  //probeReady, 以mongodb驅動直連容器對外埠試跑ping
49
56
  //不使用容器內mongosh, 因mongo映像初始化期間會先啟動僅監聽unix socket之臨時服務,
50
57
  //容器內探測會提早回報就緒, 直連TCP方為測試實際所走路徑
@@ -64,10 +71,28 @@ let probeReady = async (port) => {
64
71
  return ok
65
72
  }
66
73
 
67
- //startContainer, 創建容器並等待服務就緒, 回傳容器對外埠
74
+ //probeReadyRs, replica set容器須等到本節點成為PRIMARY方可寫入
75
+ let probeReadyRs = async (port) => {
76
+ let client = new mongodb.MongoClient(genUrlRs(port), { serverSelectionTimeoutMS: 2000 })
77
+ let ok = false
78
+ try {
79
+ let hello = await client.db('admin').command({ hello: 1 })
80
+ ok = hello.isWritablePrimary === true
81
+ }
82
+ catch (err) {
83
+ ok = false
84
+ }
85
+ finally {
86
+ await client.close().catch(() => {})
87
+ }
88
+ return ok
89
+ }
90
+
91
+ //runContainer, 創建容器並回傳對外埠
68
92
  //對外埠由getFreePort動態取得, 避免與既有MongoDB服務搶27017, 亦避免多測試檔並行時互搶同一埠
69
- //mongo映像以MONGO_INITDB_ROOT_USERNAME、MONGO_INITDB_ROOT_PASSWORD即建妥帳號, 無須另跑建置語句
70
- let startContainer = async (ctName) => {
93
+ //getFreePort取得之埠於關閉監聽至docker實際綁定之間可能已被他者佔用, 多測試檔並行時尤其容易發生,
94
+ //故綁定失敗即改取新埠重試
95
+ let runContainer = async (ctName, genArgs) => {
71
96
 
72
97
  //check docker
73
98
  await docker(['version', '--format', '{{.Server.Version}}'])
@@ -78,8 +103,7 @@ let startContainer = async (ctName) => {
78
103
  //清除前次殘留容器
79
104
  await docker(['rm', '-f', ctName]).catch(() => {})
80
105
 
81
- //run, getFreePort取得之埠於關閉監聽至docker實際綁定之間可能已被他者佔用,
82
- //多測試檔並行時尤其容易發生, 故綁定失敗即改取新埠重試
106
+ //run
83
107
  let ctPort = null
84
108
  let nTry = 10
85
109
  for (let i = 0; i < nTry; i++) {
@@ -89,7 +113,7 @@ let startContainer = async (ctName) => {
89
113
 
90
114
  //run
91
115
  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])
116
+ await docker(genArgs(ctPort))
93
117
  .catch((e) => {
94
118
  err = e
95
119
  })
@@ -111,20 +135,75 @@ let startContainer = async (ctName) => {
111
135
 
112
136
  }
113
137
 
114
- //等待服務就緒, 首次需拉取映像故給予較長時間
115
- let ready = false
138
+ return ctPort
139
+ }
140
+
141
+ //waitReady, 輪詢至就緒, 首次需拉取映像故給予較長時間
142
+ let waitReady = async (fnProbe, port, msgErr) => {
116
143
  for (let i = 0; i < 90; i++) {
117
- let ok = await probeReady(ctPort)
144
+ let ok = await fnProbe(port)
118
145
  if (ok) {
119
- ready = true
146
+ return
147
+ }
148
+ await delay(2000)
149
+ }
150
+ throw new Error(msgErr)
151
+ }
152
+
153
+ //startContainer, 創建standalone容器並等待服務就緒, 回傳容器對外埠
154
+ //mongo映像以MONGO_INITDB_ROOT_USERNAME、MONGO_INITDB_ROOT_PASSWORD即建妥帳號, 無須另跑建置語句
155
+ let startContainer = async (ctName) => {
156
+
157
+ //run
158
+ let ctPort = await runContainer(ctName, (port) => {
159
+ return ['run', '-d', '--rm', '--name', ctName, '-e', `MONGO_INITDB_ROOT_USERNAME=${ctUser}`, '-e', `MONGO_INITDB_ROOT_PASSWORD=${ctPassword}`, '-p', `${port}:27017`, ctImage]
160
+ })
161
+
162
+ //waitReady
163
+ await waitReady(probeReady, ctPort, 'mongodb容器啟動逾時')
164
+
165
+ return ctPort
166
+ }
167
+
168
+ //startContainerRs, 創建replica set容器並等待PRIMARY就緒, 回傳容器對外埠
169
+ //交易須replica set或分片叢集方可用, standalone不支援, 故驗證交易路徑須另起本容器
170
+ //本容器不開啟認證: --replSet配合MONGO_INITDB_ROOT_*須另備keyFile以供成員間內部認證, 於測試無必要
171
+ let startContainerRs = async (ctName) => {
172
+
173
+ //run
174
+ let ctPort = await runContainer(ctName, (port) => {
175
+ return ['run', '-d', '--rm', '--name', ctName, '-p', `${port}:27017`, ctImage, '--replSet', 'rs0', '--bind_ip_all']
176
+ })
177
+
178
+ //等待服務可回應, 此時尚未initiate故非PRIMARY
179
+ let up = false
180
+ for (let i = 0; i < 90; i++) {
181
+ let ok = await docker(['exec', ctName, 'mongosh', '--quiet', '--eval', 'db.adminCommand({ping:1}).ok'])
182
+ .then(() => true)
183
+ .catch(() => false)
184
+ if (ok) {
185
+ up = true
120
186
  break
121
187
  }
122
188
  await delay(2000)
123
189
  }
124
- if (!ready) {
125
- throw new Error('mongodb容器啟動逾時')
190
+ if (!up) {
191
+ throw new Error('mongodb replica set容器啟動逾時')
126
192
  }
127
193
 
194
+ //initiate, 單成員之replica set即足以啟用交易
195
+ await docker(['exec', ctName, 'mongosh', '--quiet', '--eval', 'rs.initiate({_id:"rs0",members:[{_id:0,host:"127.0.0.1:27017"}]})'])
196
+ .catch((err) => {
197
+ //已initiate者再次呼叫會報AlreadyInitialized, 屬正常
198
+ let msg = `${err.stderr || ''}${err.message || ''}`
199
+ if (msg.indexOf('AlreadyInitialized') < 0) {
200
+ throw err
201
+ }
202
+ })
203
+
204
+ //waitReady, 等待本節點成為PRIMARY
205
+ await waitReady(probeReadyRs, ctPort, 'mongodb replica set選舉逾時')
206
+
128
207
  return ctPort
129
208
  }
130
209
 
@@ -137,6 +216,8 @@ let stopContainer = async (ctName) => {
137
216
  export {
138
217
  delay,
139
218
  genUrl,
219
+ genUrlRs,
140
220
  startContainer,
221
+ startContainerRs,
141
222
  stopContainer
142
223
  }