w-serv-orm 1.1.6 → 1.1.8
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.
- package/README.md +1 -0
- package/dist/w-serv-orm.umd.js +2 -2
- package/dist/w-serv-orm.umd.js.map +1 -1
- package/docs/WServOrm.html +20 -6
- package/docs/WServOrm.mjs.html +22 -4
- package/docs/index.html +1 -1
- package/g-mOrm.mjs +1 -0
- package/package.json +3 -3
- package/schema/tables/tests.mjs +1 -1
- package/src/WServOrm.mjs +21 -3
- package/src/genTestData.mjs +11 -3
- package/src/getDb.mjs +27 -6
- package/src/getDbBackupAndRecover.mjs +27 -2
- package/src/getItemsFail.mjs +5 -0
- package/src/getMapOrm.mjs +189 -37
- package/src/getProc.mjs +79 -70
- package/src/getSettings.mjs +1 -1
- package/test/api-procOrm.test.mjs +93 -0
- package/test/unit-WServOrm.test.mjs +554 -0
- package/test/unit-getDb.test.mjs +188 -0
- package/test/unit-mapOrm.test.mjs +373 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
import { spawnSync } from 'child_process'
|
|
5
|
+
import { pathToFileURL } from 'url'
|
|
6
|
+
import getDb from '../src/getDb.mjs'
|
|
7
|
+
import br from '../src/getDbBackupAndRecover.mjs'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
//會寫出備份檔之案例以子行程執行: getDb之輸出目錄於模組載入時取cwd, 子行程之cwd設於本案暫存夾, 不寫入repo根目錄
|
|
11
|
+
let fd = path.resolve('./test/_tmp/unit-getDb')
|
|
12
|
+
let urlGetDb = pathToFileURL(path.resolve('./src/getDb.mjs')).href
|
|
13
|
+
|
|
14
|
+
function mkWoItems(log, behavior = {}) {
|
|
15
|
+
return {
|
|
16
|
+
tests: {
|
|
17
|
+
select: async () => {
|
|
18
|
+
if (behavior.select) {
|
|
19
|
+
return behavior.select()
|
|
20
|
+
}
|
|
21
|
+
return [{ id: 'a' }]
|
|
22
|
+
},
|
|
23
|
+
insert: async (d) => {
|
|
24
|
+
log.push({ fn: 'insert', table: 'tests', d })
|
|
25
|
+
return { n: d.length, nInserted: d.length, ok: 1 }
|
|
26
|
+
},
|
|
27
|
+
createStorage: async () => {
|
|
28
|
+
log.push({ fn: 'createStorage', table: 'tests' })
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function getReject(pm) {
|
|
35
|
+
try {
|
|
36
|
+
await pm
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return err
|
|
40
|
+
}
|
|
41
|
+
throw new Error('預期reject但resolve')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function quiet(fn) {
|
|
45
|
+
let logOrig = console.log
|
|
46
|
+
console.log = () => {}
|
|
47
|
+
try {
|
|
48
|
+
return await fn()
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
console.log = logOrig
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeBackup(name, content) {
|
|
56
|
+
let fp = path.join(fd, name)
|
|
57
|
+
fs.writeFileSync(fp, content, 'utf8')
|
|
58
|
+
return fp
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runChild(name, code) {
|
|
62
|
+
let dir = path.join(fd, name)
|
|
63
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
64
|
+
let fp = path.join(dir, 'run.mjs')
|
|
65
|
+
fs.writeFileSync(fp, code, 'utf8')
|
|
66
|
+
let r = spawnSync(process.execPath, [fp], { cwd: dir, encoding: 'utf8' })
|
|
67
|
+
let files = fs.readdirSync(dir).filter((v) => v.startsWith('backup-'))
|
|
68
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr, dir, files }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
describe('getDb備份與還原', function() {
|
|
73
|
+
|
|
74
|
+
before(function() {
|
|
75
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
76
|
+
fs.mkdirSync(fd, { recursive: true })
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
after(function() {
|
|
80
|
+
fs.rmSync(fd, { recursive: true, force: true })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('B01 backup之select失敗即reject', async function() {
|
|
84
|
+
//D5: 印log後reject原錯誤
|
|
85
|
+
let woItems = mkWoItems([], { select: () => Promise.reject(new Error('E-select')) })
|
|
86
|
+
let err = await quiet(() => getReject(getDb.backup(woItems, { bLog: false })))
|
|
87
|
+
assert.ok(String(err).includes('E-select'))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('B02 recover之備份檔不存在即reject', async function() {
|
|
91
|
+
//D5
|
|
92
|
+
let err = await quiet(() => getReject(getDb.recover(mkWoItems([]), path.join(fd, 'none.json'), { bLog: false })))
|
|
93
|
+
assert.ok(String(err).includes('fp is not exist'))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('B03 recover之備份內容無效即reject且不寫入', async function() {
|
|
97
|
+
//P4-2: 空檔、壞JSON、空陣列、缺name皆拒絕, 不假成功
|
|
98
|
+
for (let [name, content] of [['empty.json', ''], ['bad.json', '{bad'], ['arr0.json', '[]'], ['noname.json', '[{"x":1}]']]) {
|
|
99
|
+
let log = []
|
|
100
|
+
let err = await quiet(() => getReject(getDb.recover(mkWoItems(log), writeBackup(name, content), { bLog: false })))
|
|
101
|
+
assert.ok(String(err).includes('invalid backup'), `${name}: ${err}`)
|
|
102
|
+
assert.strict.deepEqual(log, [])
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('B04 recover之備份含woItems沒有之資料表即於寫入前reject', async function() {
|
|
107
|
+
//P4-2: 先驗全部再寫入, 不留部分還原
|
|
108
|
+
let log = []
|
|
109
|
+
let fp = writeBackup('unknown.json', JSON.stringify([{ name: 'tests', data: [{ id: 'a' }] }, { name: 'nope', data: [{ id: 'b' }] }]))
|
|
110
|
+
let err = await quiet(() => getReject(getDb.recover(mkWoItems(log), fp, { bLog: false })))
|
|
111
|
+
assert.ok(String(err).includes('nope'))
|
|
112
|
+
assert.strict.deepEqual(log, [])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('B05 recover成功', async function() {
|
|
116
|
+
//D5: 成功路徑不變
|
|
117
|
+
let log = []
|
|
118
|
+
let fp = writeBackup('ok.json', JSON.stringify([{ name: 'tests', data: [{ id: 'a' }] }]))
|
|
119
|
+
await quiet(() => getDb.recover(mkWoItems(log), fp, { bLog: false }))
|
|
120
|
+
assert.strict.deepEqual(log, [{ fn: 'insert', table: 'tests', d: [{ id: 'a' }] }])
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('B06 recover之bRela缺ds或genModelsByTabs即於寫入前reject', async function() {
|
|
124
|
+
//P4-1: 明確訊息, 不寫入
|
|
125
|
+
let log = []
|
|
126
|
+
let fp = writeBackup('rela.json', JSON.stringify([{ name: 'tests', data: [{ id: 'a' }] }]))
|
|
127
|
+
let err = await quiet(() => getReject(getDb.recover(mkWoItems(log), fp, { bRela: true, bLog: false })))
|
|
128
|
+
assert.ok(String(err).includes('genModelsByTabs'))
|
|
129
|
+
assert.strict.deepEqual(log, [])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('B07 recover之bRela帶ds與genModelsByTabs可產生model並建表寫入', async function() {
|
|
133
|
+
//P4-1: genModelsByTabs收到各表settings, 之後建表並寫入
|
|
134
|
+
let log = []
|
|
135
|
+
let calls = []
|
|
136
|
+
let ds = { tests: { keys: ['id'], settings: { id: { pk: true, type: 'STRING' } } } }
|
|
137
|
+
let genModelsByTabs = (fdModels, tabs) => {
|
|
138
|
+
calls.push({ fdModels, tabs })
|
|
139
|
+
}
|
|
140
|
+
let fp = writeBackup('rela2.json', JSON.stringify([{ name: 'tests', data: [{ id: 'a' }] }]))
|
|
141
|
+
await quiet(() => getDb.recover(mkWoItems(log), fp, { bRela: true, ds, genModelsByTabs, fdModels: './test/_tmp/unit-getDb/models', bLog: false }))
|
|
142
|
+
assert.strict.equal(calls.length, 1)
|
|
143
|
+
assert.strict.deepEqual(calls[0].tabs, { tests: ds.tests.settings })
|
|
144
|
+
assert.strict.deepEqual(log.map((v) => v.fn), ['createStorage', 'insert'])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('B08 低階recover之備份內容無效即reject', async function() {
|
|
148
|
+
//P4-2: WServOrm實例上之recover即此低階函數
|
|
149
|
+
let fp = writeBackup('bad2.json', '{bad')
|
|
150
|
+
let err = await getReject(br.recover(fp, mkWoItems([])))
|
|
151
|
+
assert.ok(String(err).includes('invalid backup'))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('B12 backup之woItems無效即reject且為Error', async function() {
|
|
155
|
+
//錯誤型別: 工具函數一律拋Error(原由pmSeries以字串reject); 空物件與空陣列原會產出recover拒收之空備份, 一併拒絕
|
|
156
|
+
for (let bad of [null, undefined, 'x', 1, [], {}]) {
|
|
157
|
+
let err = await quiet(() => getReject(br.backup(bad)))
|
|
158
|
+
assert.ok(err instanceof Error && err.message === 'invalid woItems', `低階 ${String(bad)}: ${err}`)
|
|
159
|
+
err = await quiet(() => getReject(getDb.backup(bad, { bLog: false })))
|
|
160
|
+
assert.ok(err instanceof Error && err.message === 'invalid woItems', `getDb ${String(bad)}: ${err}`)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('B09 backup成功寫出備份檔(子行程)', async function() {
|
|
165
|
+
//D5: 成功路徑不變
|
|
166
|
+
let r = runChild('bk-ok', `import getDb from '${urlGetDb}'\nlet woItems = { tests: { select: async () => [{ id: 'a' }] } }\nawait getDb.backup(woItems, { bLog: false })\nconsole.log('RESOLVED')\n`)
|
|
167
|
+
assert.strict.equal(r.status, 0, r.stderr)
|
|
168
|
+
assert.ok(r.stdout.includes('RESOLVED'))
|
|
169
|
+
assert.strict.equal(r.files.length, 1)
|
|
170
|
+
let j = JSON.parse(fs.readFileSync(path.join(r.dir, r.files[0]), 'utf8'))
|
|
171
|
+
assert.strict.deepEqual(j, [{ name: 'tests', data: [{ id: 'a' }] }])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('B10 仿g_backupDb未await呼叫而失敗時離開碼非零(子行程)', async function() {
|
|
175
|
+
//D5: 使用端之頂層呼叫未await, 失敗成為未處理之rejection
|
|
176
|
+
let r = runChild('bk-fail', `import getDb from '${urlGetDb}'\nlet woItems = { tests: { select: async () => { throw new Error('E-select') } } }\ngetDb.backup(woItems)\n`)
|
|
177
|
+
assert.notStrictEqual(r.status, 0)
|
|
178
|
+
assert.strict.equal(r.files.length, 0)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('B11 backup資料無法序列化即reject且不寫出空檔(子行程)', async function() {
|
|
182
|
+
//D5: o2j失敗回空字串, 不得寫出空備份而回報成功
|
|
183
|
+
let r = runChild('bk-circular', `import getDb from '${urlGetDb}'\nlet a = { id: 'a' }\na.self = a\nlet woItems = { tests: { select: async () => [a] } }\nlet out = 'RESOLVED'\nawait getDb.backup(woItems, { bLog: false }).catch(() => { out = 'REJECTED' })\nconsole.log(out)\n`)
|
|
184
|
+
assert.ok(r.stdout.includes('REJECTED'), r.stdout + r.stderr)
|
|
185
|
+
assert.strict.equal(r.files.length, 0)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
})
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import WServOrm from '../src/WServOrm.mjs'
|
|
3
|
+
import genTestData from '../src/genTestData.mjs'
|
|
4
|
+
import dsSchema from '../schema/index.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
//替身ORM, 回傳形狀依w-orm系列規格(資料庫函數回傳定義計算.md T2、T4):
|
|
8
|
+
//save與del回傳與輸入等長保序之逐筆陣列, 單筆失敗時整批仍resolve, 該筆以ok為0並附err回報; 整批性錯誤則reject
|
|
9
|
+
let plan = {}
|
|
10
|
+
let calls = []
|
|
11
|
+
function WOrmStub() {
|
|
12
|
+
let ev = {}
|
|
13
|
+
for (let fn of ['select', 'insert', 'save', 'del', 'delAll']) {
|
|
14
|
+
ev[fn] = async (data) => {
|
|
15
|
+
calls.push({ fn, data })
|
|
16
|
+
return plan[fn](data)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return ev
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let ds = {
|
|
23
|
+
tests: {
|
|
24
|
+
keys: ['id', 'name', 'timeCreate', 'timeUpdate'],
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let okSave = { n: 1, nInserted: 0, nModified: 1, ok: 1 }
|
|
29
|
+
let okDel = { n: 1, nDeleted: 1, ok: 1 }
|
|
30
|
+
|
|
31
|
+
async function getReject(pm) {
|
|
32
|
+
try {
|
|
33
|
+
await pm
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return err
|
|
37
|
+
}
|
|
38
|
+
throw new Error('預期reject但resolve')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
describe('mapOrm逐筆失敗', function() {
|
|
43
|
+
|
|
44
|
+
let so = WServOrm(ds, WOrmStub, 'stub-url', 'stub-db', { useCheckUser: false, useExcludeWhenNotAdmin: false })
|
|
45
|
+
let proc = (mode, input) => so.procOrm('u1', 'tests', mode, input)
|
|
46
|
+
|
|
47
|
+
beforeEach(function() {
|
|
48
|
+
plan = {}
|
|
49
|
+
calls = []
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('U01 save陣列全部成功', async function() {
|
|
53
|
+
//R04: 全部成功時save回傳不變, 僅回傳id
|
|
54
|
+
plan.save = (data) => data.map(() => okSave)
|
|
55
|
+
let r = await proc('save', [{ id: 's1', name: 'a' }, { id: 's2', name: 'b' }])
|
|
56
|
+
assert.strict.deepEqual(r, [{ id: 's1' }, { id: 's2' }])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('U02 save物件全部成功', async function() {
|
|
60
|
+
//R04: 輸入為單一物件時回傳單一{id}
|
|
61
|
+
plan.save = () => [okSave]
|
|
62
|
+
let r = await proc('save', { id: 's1', name: 'a' })
|
|
63
|
+
assert.strict.deepEqual(r, { id: 's1' })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('U03 save陣列中一筆ok為0', async function() {
|
|
67
|
+
//R01: 逐筆ok為0即reject, 訊息帶失敗筆之主鍵, 不含成功筆
|
|
68
|
+
plan.save = () => [okSave, { n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-save' }]
|
|
69
|
+
let err = await getReject(proc('save', [{ id: 's1' }, { id: 's2' }]))
|
|
70
|
+
assert.strict.equal(err, 'failed to save data: id[s2]')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('U04 save物件ok為0', async function() {
|
|
74
|
+
//R01: 輸入為單一物件時ORM回傳長度為1之陣列, 仍須檢出
|
|
75
|
+
plan.save = () => [{ n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-save' }]
|
|
76
|
+
let err = await getReject(proc('save', { id: 's1' }))
|
|
77
|
+
assert.strict.equal(err, 'failed to save data: id[s1]')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('U05 save未帶主鍵之筆ok為0且無err', async function() {
|
|
81
|
+
//R01: 無有效主鍵者以輸入位置標示; R07: ok為0而無err(舊版ORM)仍判失敗
|
|
82
|
+
plan.save = () => [okSave, { n: 1, ok: 0 }]
|
|
83
|
+
let err = await getReject(proc('save', [{ id: 's1' }, { name: 'no-id' }]))
|
|
84
|
+
assert.strict.equal(err, 'failed to save data: data[1]')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('U06 save整批reject', async function() {
|
|
88
|
+
//R05: 整批性錯誤之訊息不變
|
|
89
|
+
plan.save = () => Promise.reject(new Error('E-batch'))
|
|
90
|
+
let err = await getReject(proc('save', [{ id: 's1' }]))
|
|
91
|
+
assert.strict.equal(err, 'failed to save data')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('U07 save之ORM回傳非陣列', async function() {
|
|
95
|
+
//R07: 未依規格之ORM回傳非陣列時不檢查, 照舊回傳id
|
|
96
|
+
plan.save = () => ({ n: 1, ok: 1 })
|
|
97
|
+
let r = await proc('save', [{ id: 's1' }])
|
|
98
|
+
assert.strict.deepEqual(r, [{ id: 's1' }])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('U08 save多筆ok為0', async function() {
|
|
102
|
+
//R08: 失敗筆數超過10筆時僅列前10筆並附總筆數
|
|
103
|
+
let input = []
|
|
104
|
+
for (let i = 1; i <= 12; i++) {
|
|
105
|
+
input.push({ id: `f${i}` })
|
|
106
|
+
}
|
|
107
|
+
plan.save = (data) => data.map(() => ({ n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-full' }))
|
|
108
|
+
let err = await getReject(proc('save', input))
|
|
109
|
+
let ids = input.slice(0, 10).map((v) => `id[${v.id}]`).join(', ')
|
|
110
|
+
assert.strict.equal(err, `failed to save data: ${ids}, ... (12 in total)`)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('U09 del全部成功含主鍵未命中', async function() {
|
|
114
|
+
//R04: del回傳ORM原始逐筆陣列不變; 主鍵未命中(n為0、ok為1)屬正常結果而非失敗
|
|
115
|
+
let rs = [okDel, { n: 0, nDeleted: 0, ok: 1 }]
|
|
116
|
+
plan.del = () => rs
|
|
117
|
+
let r = await proc('del', [{ id: 'd1' }, { id: 'd-none' }])
|
|
118
|
+
assert.strict.deepEqual(r, rs)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('U10 del數字主鍵ok為0', async function() {
|
|
122
|
+
//R02: del逐筆ok為0即reject; 數字主鍵照列
|
|
123
|
+
plan.del = () => [okDel, { n: 0, nDeleted: 0, ok: 0, err: 'can not delete by invalid id[123]' }]
|
|
124
|
+
let err = await getReject(proc('del', [{ id: 'd1' }, { id: 123 }]))
|
|
125
|
+
assert.strict.equal(err, 'failed to delete data: id[123]')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('U10b del物件輸入ok為0且無err', async function() {
|
|
129
|
+
//R02: 物件輸入亦須檢出; R07: ok為0而無err仍判失敗
|
|
130
|
+
plan.del = () => [{ n: 1, nDeleted: 0, ok: 0 }]
|
|
131
|
+
let err = await getReject(proc('del', { id: 'd1' }))
|
|
132
|
+
assert.strict.equal(err, 'failed to delete data: id[d1]')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('U11 del整批reject', async function() {
|
|
136
|
+
//R05: 整批性錯誤之訊息不變
|
|
137
|
+
plan.del = () => Promise.reject(new Error('E-batch'))
|
|
138
|
+
let err = await getReject(proc('del', [{ id: 'd1' }]))
|
|
139
|
+
assert.strict.equal(err, 'failed to delete data')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('U12 del之ORM回傳非陣列', async function() {
|
|
143
|
+
//R07: 未依規格之ORM回傳非陣列時不檢查, 照舊回傳原值
|
|
144
|
+
plan.del = () => ({ n: 1, nDeleted: 1, ok: 1 })
|
|
145
|
+
let r = await proc('del', [{ id: 'd1' }])
|
|
146
|
+
assert.strict.deepEqual(r, { n: 1, nDeleted: 1, ok: 1 })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('U13 mix全部成功', async function() {
|
|
150
|
+
//R04: mix依add、del、diff順序執行, 回傳累積之id
|
|
151
|
+
plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
|
|
152
|
+
plan.del = (data) => data.map(() => okDel)
|
|
153
|
+
plan.save = (data) => data.map(() => okSave)
|
|
154
|
+
let r = await proc('mix', { add: [{ id: 'a1' }], del: [{ id: 'd1' }], diff: [{ id: 'f1' }] })
|
|
155
|
+
assert.strict.deepEqual(r, [{ id: 'a1' }, { id: 'd1' }, { id: 'f1' }])
|
|
156
|
+
assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'del', 'save'])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('U14 mix之del與diff皆有ok為0', async function() {
|
|
160
|
+
//R03: 逐筆失敗不中斷後續步驟, 全部步驟完成後一次reject並分步列出
|
|
161
|
+
plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
|
|
162
|
+
plan.del = () => [okDel, { n: 0, nDeleted: 0, ok: 0, err: 'E-del' }]
|
|
163
|
+
plan.save = () => [{ n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-save' }]
|
|
164
|
+
let err = await getReject(proc('mix', { add: [{ id: 'a1' }], del: [{ id: 'd1' }, { id: 'd2' }], diff: [{ id: 'f1' }] }))
|
|
165
|
+
assert.strict.equal(err, 'failed to delete data: id[d2]; failed to update data: id[f1]')
|
|
166
|
+
assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'del', 'save'])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('U15 mix僅del有ok為0', async function() {
|
|
170
|
+
//R03: del之逐筆失敗不中斷diff, diff仍寫入, 最後reject僅列del之失敗
|
|
171
|
+
plan.del = () => [{ n: 0, nDeleted: 0, ok: 0, err: 'E-del' }]
|
|
172
|
+
plan.save = (data) => data.map(() => okSave)
|
|
173
|
+
let err = await getReject(proc('mix', { del: [{ id: 'd1' }], diff: [{ id: 'f1' }] }))
|
|
174
|
+
assert.strict.equal(err, 'failed to delete data: id[d1]')
|
|
175
|
+
assert.strict.deepEqual(calls.map((v) => v.fn), ['del', 'save'])
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('U16 mix之del整批reject', async function() {
|
|
179
|
+
//R05: 整批性錯誤維持立即reject, 後續步驟不執行
|
|
180
|
+
plan.del = () => Promise.reject(new Error('E-batch'))
|
|
181
|
+
plan.save = (data) => data.map(() => okSave)
|
|
182
|
+
let err = await getReject(proc('mix', { del: [{ id: 'd1' }], diff: [{ id: 'f1' }] }))
|
|
183
|
+
assert.strict.equal(err, 'failed to delete data')
|
|
184
|
+
assert.strict.deepEqual(calls.map((v) => v.fn), ['del'])
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('U17 mix之add整批reject', async function() {
|
|
188
|
+
//R05: insert整批性錯誤維持立即reject, 後續步驟不執行
|
|
189
|
+
plan.insert = () => Promise.reject(new Error('E-batch'))
|
|
190
|
+
plan.del = (data) => data.map(() => okDel)
|
|
191
|
+
let err = await getReject(proc('mix', { add: [{ id: 'a1' }], del: [{ id: 'd1' }] }))
|
|
192
|
+
assert.strict.equal(err, 'failed to insert data')
|
|
193
|
+
assert.strict.deepEqual(calls.map((v) => v.fn), ['insert'])
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('U18 mix之del有ok為0後diff整批reject', async function() {
|
|
197
|
+
//R05: 遇整批性錯誤即以該步驟之訊息reject, 先前之逐筆失敗已由log輸出
|
|
198
|
+
plan.del = () => [{ n: 0, nDeleted: 0, ok: 0, err: 'E-del' }]
|
|
199
|
+
plan.save = () => Promise.reject(new Error('E-batch'))
|
|
200
|
+
let err = await getReject(proc('mix', { del: [{ id: 'd1' }], diff: [{ id: 'f1' }] }))
|
|
201
|
+
assert.strict.equal(err, 'failed to update data')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('U19 insert成功', async function() {
|
|
205
|
+
//R04: insert回傳聚合物件而非逐筆陣列, 照舊回傳id
|
|
206
|
+
plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
|
|
207
|
+
let r = await proc('insert', [{ id: 'i1' }])
|
|
208
|
+
assert.strict.deepEqual(r, [{ id: 'i1' }])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('U22 錯誤原文寫入log而不進reject訊息', async function() {
|
|
212
|
+
//R06: 前端訊息只帶主鍵, 錯誤原文由console.log輸出
|
|
213
|
+
plan.save = () => [{ n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-secret-detail' }]
|
|
214
|
+
let logs = []
|
|
215
|
+
let logOrig = console.log
|
|
216
|
+
console.log = (...args) => {
|
|
217
|
+
logs.push(args)
|
|
218
|
+
}
|
|
219
|
+
let err
|
|
220
|
+
try {
|
|
221
|
+
err = await getReject(proc('save', [{ id: 's1' }]))
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
console.log = logOrig
|
|
225
|
+
}
|
|
226
|
+
assert.ok(!String(err).includes('E-secret-detail'))
|
|
227
|
+
assert.ok(JSON.stringify(logs).includes('E-secret-detail'))
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
describe('mapOrm主鍵與clearXSS', function() {
|
|
234
|
+
|
|
235
|
+
let so = WServOrm(ds, WOrmStub, 'stub-url', 'stub-db', { useCheckUser: false, useExcludeWhenNotAdmin: false })
|
|
236
|
+
let proc = (mode, input) => so.procOrm('u1', 'tests', mode, input)
|
|
237
|
+
|
|
238
|
+
beforeEach(function() {
|
|
239
|
+
plan = {}
|
|
240
|
+
calls = []
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('X01 數學式之主鍵與欄位原樣寫入並回傳', async function() {
|
|
244
|
+
//wsemi 1.9.13起clearXSS只轉義會開始標籤之<, 數學式不改寫, 主鍵不被拒絕且與寫入者一致
|
|
245
|
+
plan.save = (data) => data.map(() => okSave)
|
|
246
|
+
let r = await proc('save', [{ id: 'a < b', name: '溫度 > 30°C' }, { id: '1 > 0', name: 'x <= 5' }])
|
|
247
|
+
assert.strict.deepEqual(r, [{ id: 'a < b' }, { id: '1 > 0' }])
|
|
248
|
+
assert.strict.deepEqual(calls[0].data.map((v) => [v.id, v.name]), [['a < b', '溫度 > 30°C'], ['1 > 0', 'x <= 5']])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('X02 save之主鍵會被改寫即整批reject且不呼叫ORM', async function() {
|
|
252
|
+
//改寫後之主鍵與呼叫端所持者不同而查不到, 整批拒絕; 訊息只列位置不回顯主鍵; 結尾之<亦會被轉義
|
|
253
|
+
plan.save = (data) => data.map(() => okSave)
|
|
254
|
+
let err = await getReject(proc('save', [{ id: 's1' }, { id: 'a<b' }, { id: 'x <' }]))
|
|
255
|
+
assert.strict.equal(err, 'failed to save data, id contains HTML markup: data[1], data[2]')
|
|
256
|
+
assert.strict.deepEqual(calls, [])
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('X03 insert物件之主鍵會被改寫即reject且不呼叫ORM', async function() {
|
|
260
|
+
plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
|
|
261
|
+
let err = await getReject(proc('insert', { id: '<script>x' }))
|
|
262
|
+
assert.strict.equal(err, 'failed to insert data, id contains HTML markup: data[0]')
|
|
263
|
+
assert.strict.deepEqual(calls, [])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('X04 mix之新增或修改之主鍵會被改寫即於任何步驟之前reject', async function() {
|
|
267
|
+
//mix依add、del、diff執行, 預檢於任何步驟寫入之前, 故一筆都不寫入
|
|
268
|
+
plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
|
|
269
|
+
plan.del = (data) => data.map(() => okDel)
|
|
270
|
+
plan.save = (data) => data.map(() => okSave)
|
|
271
|
+
let err = await getReject(proc('mix', { add: [{ id: 'a1' }, { id: 'a<b' }], del: [{ id: 'd1' }], diff: [{ id: '</x>' }] }))
|
|
272
|
+
assert.strict.equal(err, 'failed to insert data, id contains HTML markup: data[1]; failed to update data, id contains HTML markup: data[0]')
|
|
273
|
+
assert.strict.deepEqual(calls, [])
|
|
274
|
+
err = await getReject(proc('mix', { del: [{ id: 'd1' }], diff: [{ id: 'f1' }, { id: '<!--x-->' }] }))
|
|
275
|
+
assert.strict.equal(err, 'failed to update data, id contains HTML markup: data[1]')
|
|
276
|
+
assert.strict.deepEqual(calls, [])
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('X05 數字主鍵、未帶主鍵與不被改寫之主鍵照常寫入', async function() {
|
|
280
|
+
//判準為是否被改寫: 數字不經清除, 未帶主鍵者由本套件補值, 白名單標籤之規範寫法不被改寫
|
|
281
|
+
plan.save = (data) => data.map(() => okSave)
|
|
282
|
+
let r = await proc('save', [{ id: 5 }, { id: '<b>粗體</b>' }, { name: 'no-id' }])
|
|
283
|
+
assert.strict.equal(r[0].id, 5)
|
|
284
|
+
assert.strict.equal(r[1].id, '<b>粗體</b>')
|
|
285
|
+
assert.ok(typeof r[2].id === 'string' && r[2].id.length > 0)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('X06 select與del不經clearXSS, 主鍵原樣交ORM', async function() {
|
|
289
|
+
//此不對稱即主鍵會被改寫者須於寫入前拒絕之原因
|
|
290
|
+
plan.select = () => []
|
|
291
|
+
plan.del = (data) => data.map(() => ({ n: 0, nDeleted: 0, ok: 1 }))
|
|
292
|
+
await proc('select', { id: 'a<b' })
|
|
293
|
+
await proc('del', [{ id: 'a<b' }])
|
|
294
|
+
assert.strict.deepEqual(calls.map((v) => v.data), [{ id: 'a<b' }, [{ id: 'a<b' }]])
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('X07 insert與mix各入口之其餘主鍵狀態', async function() {
|
|
298
|
+
//數字與不被改寫之主鍵照寫, mix之修改未帶主鍵者補值, mix之刪除不經clearXSS而原樣交ORM
|
|
299
|
+
plan.insert = (data) => ({ n: data.length, nInserted: data.length, ok: 1 })
|
|
300
|
+
plan.del = (data) => data.map(() => okDel)
|
|
301
|
+
plan.save = (data) => data.map(() => okSave)
|
|
302
|
+
let r1 = await proc('insert', [{ id: 7 }, { id: 'a < b' }])
|
|
303
|
+
assert.strict.deepEqual(r1, [{ id: 7 }, { id: 'a < b' }])
|
|
304
|
+
let r2 = await proc('mix', { add: [{ id: 8 }, { id: '1 > 0' }], del: [{ id: 'a<b' }], diff: [{ id: 9 }, { id: 'x <= 5' }, { name: 'no-id' }] })
|
|
305
|
+
assert.strict.deepEqual(r2.slice(0, 5), [{ id: 8 }, { id: '1 > 0' }, { id: 'a<b' }, { id: 9 }, { id: 'x <= 5' }])
|
|
306
|
+
assert.ok(typeof r2[5].id === 'string' && r2[5].id.length > 0)
|
|
307
|
+
assert.strict.deepEqual(calls.map((v) => v.fn), ['insert', 'insert', 'del', 'save'])
|
|
308
|
+
assert.strict.deepEqual(calls[2].data, [{ id: 'a<b' }])
|
|
309
|
+
assert.strict.equal(calls[3].data[2].id, r2[5].id)
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
describe('genTestData逐筆失敗', function() {
|
|
316
|
+
|
|
317
|
+
let dsGen = {
|
|
318
|
+
tests: {
|
|
319
|
+
funTest: () => [{ id: 'g1' }, { id: 'g2' }],
|
|
320
|
+
},
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
it('U20 funTest全部成功', async function() {
|
|
324
|
+
//R09: 全部成功時照舊resolve, 並以funTest產生之數據呼叫save
|
|
325
|
+
let saved = null
|
|
326
|
+
let woItems = {
|
|
327
|
+
tests: {
|
|
328
|
+
save: async (rs) => {
|
|
329
|
+
saved = rs
|
|
330
|
+
return rs.map(() => okSave)
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
await genTestData(dsGen, woItems, { mode: 'funTest', bLog: false })
|
|
335
|
+
assert.strict.deepEqual(saved, [{ id: 'g1' }, { id: 'g2' }])
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('U21 funTest之save有ok為0', async function() {
|
|
339
|
+
//R09: 逐筆ok為0即reject, 訊息含資料表名與錯誤原文(伺服器端開發工具)
|
|
340
|
+
let woItems = {
|
|
341
|
+
tests: {
|
|
342
|
+
save: async () => [okSave, { n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-gen' }],
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
let err = await getReject(genTestData(dsGen, woItems, { mode: 'funTest', bLog: false }))
|
|
346
|
+
assert.ok(String(err).includes('tests'))
|
|
347
|
+
assert.ok(String(err).includes('E-gen'))
|
|
348
|
+
assert.ok(String(err).includes('g2'))
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('U23 funTestAndSave與funTest之逐筆失敗訊息相同', async function() {
|
|
352
|
+
//R09: funTestAndSave由w-data-collector之build產生(schema/index.mjs)且其錯誤原樣轉出; w-data-collector 1.0.46起兩種模式之訊息同為英文同格式
|
|
353
|
+
let woItems = {
|
|
354
|
+
tests: {
|
|
355
|
+
save: async (rs) => rs.map((v, k) => (k === 1 ? { n: 1, nInserted: 0, nModified: 0, ok: 0, err: 'E-gen' } : okSave)),
|
|
356
|
+
},
|
|
357
|
+
}
|
|
358
|
+
let logOrig = console.log
|
|
359
|
+
console.log = () => {}
|
|
360
|
+
let errs = []
|
|
361
|
+
try {
|
|
362
|
+
for (let mode of ['funTestAndSave', 'funTest']) {
|
|
363
|
+
errs.push(await getReject(genTestData(dsSchema, woItems, { mode, bLog: false })))
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
console.log = logOrig
|
|
368
|
+
}
|
|
369
|
+
let msg = 'failed to write test data into table[tests]: [{"k":1,"id":"id-for-tests-2","err":"E-gen"}]'
|
|
370
|
+
assert.strict.deepEqual(errs.map((e) => e.message), [msg, msg])
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
})
|