w-serv-orm 1.1.11 → 1.1.13
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/dist/w-serv-orm.umd.js +2 -2
- package/dist/w-serv-orm.umd.js.map +1 -1
- package/docs/WServOrm.html +5 -4
- package/docs/WServOrm.mjs.html +4 -3
- package/docs/index.html +1 -1
- package/package.json +3 -3
- package/src/WServOrm.mjs +3 -2
- package/src/getDbBackupAndRecover.mjs +1 -2
- package/src/getMapOrm.mjs +247 -33
- package/src/getWoItems.mjs +4 -4
- package/test/api-objcopy.test.mjs +232 -0
- package/test/tools/ormStub.mjs +115 -0
- package/test/unit-getDb.test.mjs +11 -0
- package/test/unit-objcopy.test.mjs +907 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import vm from 'vm'
|
|
3
|
+
import util from 'util'
|
|
4
|
+
import WServOrm from '../src/WServOrm.mjs'
|
|
5
|
+
import getMapOrm from '../src/getMapOrm.mjs'
|
|
6
|
+
import { createStub, getReject, quiet, same } from './tools/ormStub.mjs'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
//R-OBJ-01 使用端物件之迭代與複製(L01-L04、P01-P03)、R-INPUT-01 規則3 之 select 與 delAll(D01-D03)
|
|
10
|
+
//替身ORM記錄ORM收到之實參(spy); 規格之來源: CLAUDE_rulebook.md 之 R-OBJ-01、R-INPUT-01, 及 dspec「w-orm系列條件語法支援盤點.md」一之一
|
|
11
|
+
let stub = createStub()
|
|
12
|
+
let { WOrmStub, calls } = stub
|
|
13
|
+
|
|
14
|
+
let dsStr = { tests: { keys: ['id', 'name', 'length'], settings: { id: { pk: true, type: 'STRING' }, name: { type: 'STRING' } } } }
|
|
15
|
+
let dsUnknown = { tests: { keys: ['id', 'name', 'length'] } }
|
|
16
|
+
let dsInt = { tests: { keys: ['id', 'name', 'length'], settings: { id: { pk: true, type: 'INTEGER' }, name: { type: 'STRING' } } } }
|
|
17
|
+
let isGen = (v) => typeof v === 'string' && v.length > 30
|
|
18
|
+
let wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
19
|
+
let clone = (v) => structuredClone(v)
|
|
20
|
+
|
|
21
|
+
//kinds, 四種資料表型態: 字串型主鍵(轉型與補值)、型別未知(同)、整數型主鍵(皆不)、ds無該表(皆不; 直接呼叫getMapOrm令ds與woItems不一致)
|
|
22
|
+
function getKinds() {
|
|
23
|
+
return [
|
|
24
|
+
{ kind: 'str', cast: true, proc: WServOrm(dsStr, WOrmStub, 'u', 'd').procOrm },
|
|
25
|
+
{ kind: 'unknown', cast: true, proc: WServOrm(dsUnknown, WOrmStub, 'u', 'd').procOrm },
|
|
26
|
+
{ kind: 'int', cast: false, proc: WServOrm(dsInt, WOrmStub, 'u', 'd').procOrm },
|
|
27
|
+
{ kind: 'none', cast: false, proc: getMapOrm({ others: dsStr.tests }, { tests: WOrmStub() }) },
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//parse, JSON.parse 才能產生自有 __proto__ 鍵(物件字面量之 __proto__ 為設原型)
|
|
32
|
+
let parse = (c) => JSON.parse(c)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
describe('R-OBJ-01 使用端物件之迭代與複製', function() {
|
|
36
|
+
|
|
37
|
+
beforeEach(function() {
|
|
38
|
+
stub.reset()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
//find 語料: [送入之find, 轉型表(str、unknown)時ORM應收到者]; 轉型只改id之有限數字(R-ID-02), 其餘原樣
|
|
42
|
+
let finds = [
|
|
43
|
+
[{ length: 1, name: 'x' }],
|
|
44
|
+
[{ length: 0 }],
|
|
45
|
+
[{ length: 0, name: 'alpha' }],
|
|
46
|
+
[{ id: 'a1', length: 3 }],
|
|
47
|
+
[{ id: 5, length: 2 }, { id: '5', length: 2 }],
|
|
48
|
+
[{ $and: [{ length: 1, name: 'x' }], $or: [{ length: 2 }, { id: 7, length: 1 }], $nor: [{ id: 'z', length: 3 }] }, { $and: [{ length: 1, name: 'x' }], $or: [{ length: 2 }, { id: '7', length: 1 }], $nor: [{ id: 'z', length: 3 }] }],
|
|
49
|
+
[{ id: { length: 2, $in: [1, 2] } }, { id: { length: 2, $in: ['1', '2'] } }],
|
|
50
|
+
[{ id: { $not: { length: 1, $eq: 9 } } }, { id: { $not: { length: 1, $eq: '9' } } }],
|
|
51
|
+
[{ name: { length: 1, $regex: '^a' } }],
|
|
52
|
+
//對照: 非陣列樣之length(非整數、負數、字串、null)修前修後皆原樣
|
|
53
|
+
[{ length: 3.5, name: 'x' }],
|
|
54
|
+
[{ length: -1, name: 'x' }],
|
|
55
|
+
[{ length: '3', name: 'x' }],
|
|
56
|
+
[{ length: null, name: 'x' }],
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
it('L01 select與delAll之find含整數length鍵: ORM收到之實參與送入者相同(id之有限數字轉字串除外)', async function() {
|
|
60
|
+
//R-OBJ-01 規則1: 修前 {length:1,name:'x'} 被當陣列樣迭代而成 {0:undefined}, 條件整個消失(select回全表、delAll刪全表)
|
|
61
|
+
for (let { kind, cast, proc } of getKinds()) {
|
|
62
|
+
for (let mode of ['select', 'delAll']) {
|
|
63
|
+
for (let [k, [find, expectCast]] of finds.entries()) {
|
|
64
|
+
stub.reset()
|
|
65
|
+
await proc('u1', 'tests', mode, find)
|
|
66
|
+
assert.strictEqual(calls.length, 1, `${kind} ${mode} #${k}`)
|
|
67
|
+
assert.strictEqual(calls[0].fn, mode)
|
|
68
|
+
same(calls[0].data, cast && expectCast ? expectCast : find, `${kind} ${mode} #${k} ORM收到`)
|
|
69
|
+
if (!cast) {
|
|
70
|
+
//整數型主鍵與ds無該表者不複製, 原物件交ORM(故內容斷言恆真, 鑑別力來自str、unknown兩種; 此處明示不複製)
|
|
71
|
+
assert.strictEqual(calls[0].data, find, `${kind} ${mode} #${k} 應為同一參考`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('L02 find之length為極大整數: 不依length迭代, 立即完成且ORM收到之實參不變', async function() {
|
|
79
|
+
//R-OBJ-01 規則1: lodash each 之迭代次數即 length 之值(2e7次約61 ms, 2^53-1 實質不終止), 可由資料通道送入而癱瘓事件迴圈
|
|
80
|
+
//先以 2e6 (修前亦可完成, 修前以內容不符轉紅而不至於不終止)再以 2^53-1
|
|
81
|
+
for (let { kind, proc } of getKinds()) {
|
|
82
|
+
for (let mode of ['select', 'delAll']) {
|
|
83
|
+
let f1 = { length: 2e6, name: 'x' }
|
|
84
|
+
stub.reset()
|
|
85
|
+
await proc('u1', 'tests', mode, f1)
|
|
86
|
+
same(calls[0].data, { length: 2e6, name: 'x' }, `${kind} ${mode} length=2e6`)
|
|
87
|
+
|
|
88
|
+
let f2 = { length: Number.MAX_SAFE_INTEGER, name: 'x' }
|
|
89
|
+
stub.reset()
|
|
90
|
+
let t = Date.now()
|
|
91
|
+
await proc('u1', 'tests', mode, f2)
|
|
92
|
+
assert.ok(Date.now() - t < 1000, `${kind} ${mode} length=2^53-1 耗時 ${Date.now() - t} ms`)
|
|
93
|
+
same(calls[0].data, { length: Number.MAX_SAFE_INTEGER, name: 'x' }, `${kind} ${mode} length=2^53-1`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
//rows 語料: 含整數length欄之列、非整數length、無length、無主鍵
|
|
99
|
+
let rows = [
|
|
100
|
+
{ id: 'l1', name: 'x', length: 3 },
|
|
101
|
+
{ id: 'l2', name: 'y', length: 'abc' },
|
|
102
|
+
{ id: 'l3', name: 'z' },
|
|
103
|
+
{ name: 'noid', length: 0 },
|
|
104
|
+
{ length: 2 },
|
|
105
|
+
{ id: 'l5', length: 2 }, //已帶主鍵、無其他欄位: 主鍵須原樣(外審 sol F5: 不得被錯改)
|
|
106
|
+
{ id: 'l6', length: Number.MAX_SAFE_INTEGER },
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
//getSent, 取ORM收到之列; fn為寫入之函數名
|
|
110
|
+
let runWrite = async (proc, how, input) => {
|
|
111
|
+
stub.reset()
|
|
112
|
+
let r
|
|
113
|
+
if (how === 'insert' || how === 'save') {
|
|
114
|
+
r = await proc('u1', 'tests', how, input)
|
|
115
|
+
return { r, sent: calls[0].data }
|
|
116
|
+
}
|
|
117
|
+
r = await proc('u1', 'tests', 'mix', how === 'mix.add' ? { add: input } : { diff: input })
|
|
118
|
+
return { r, sent: calls[0].data }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
it('L03 寫入列含整數length欄: 原樣交ORM, 回傳主鍵與ORM收到者一致, 不補或只補缺的主鍵', async function() {
|
|
122
|
+
//R-OBJ-01 規則1: 修前整列內容遺失(insert、mix回傳之主鍵被換成產生值; save單筆之列變{}而不寫入並回報{id:undefined})
|
|
123
|
+
for (let { kind, cast, proc } of getKinds()) {
|
|
124
|
+
if (kind === 'none') {
|
|
125
|
+
continue //ds無該表時adjustData拋錯(既有行為), 與本規則無關
|
|
126
|
+
}
|
|
127
|
+
for (let how of ['insert', 'save', 'mix.add', 'mix.diff']) {
|
|
128
|
+
let { r, sent } = await runWrite(proc, how, rows)
|
|
129
|
+
assert.strictEqual(sent.length, rows.length, `${kind} ${how}`)
|
|
130
|
+
rows.forEach((row, i) => {
|
|
131
|
+
let got = sent[i]
|
|
132
|
+
let { id: idGot, ...restGot } = got
|
|
133
|
+
let { id: idRow, ...restRow } = row
|
|
134
|
+
same(restGot, restRow, `${kind} ${how} 列${i}之其餘欄位`)
|
|
135
|
+
if (typeof idRow === 'string') {
|
|
136
|
+
assert.strictEqual(idGot, idRow, `${kind} ${how} 列${i}之主鍵`)
|
|
137
|
+
}
|
|
138
|
+
else if (cast) {
|
|
139
|
+
assert.ok(isGen(idGot), `${kind} ${how} 列${i}缺主鍵者補值: ${idGot}`)
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
assert.strictEqual(idGot, undefined, `${kind} ${how} 列${i}整數型主鍵欄不補值`)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
same(r, sent.map((v) => ({ id: v.id })), `${kind} ${how} 回傳主鍵`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
//單一物件(insert、save)
|
|
149
|
+
for (let how of ['insert', 'save']) {
|
|
150
|
+
let { r, sent } = await runWrite(proc, how, { id: 's1', name: 'x', length: 1 })
|
|
151
|
+
same(sent, { id: 's1', name: 'x', length: 1 }, `${kind} ${how} 單一物件`)
|
|
152
|
+
same(r, { id: 's1' }, `${kind} ${how} 單一物件回傳`)
|
|
153
|
+
let r2 = await runWrite(proc, how, { length: 0 })
|
|
154
|
+
if (cast) {
|
|
155
|
+
assert.ok(isGen(r2.sent.id) && r2.r.id === r2.sent.id, `${kind} ${how} 單一{length:0}補值`)
|
|
156
|
+
same({ ...r2.sent, id: 'G' }, { length: 0, id: 'G' }, `${kind} ${how} 單一{length:0}其餘欄位`)
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
same(r2.sent, { length: 0 }, `${kind} ${how} 單一{length:0}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('L04 呼叫端之find與寫入列不被改動(ds無稽核欄位者)', async function() {
|
|
166
|
+
//R-OBJ-01 規則2、R-ID-02 規則4: 複本寫於新物件
|
|
167
|
+
//本案之ds不含稽核欄位(userId、timeCreate、userIdUpdate、timeUpdate): ds含稽核欄位時adjustData直接賦值於呼叫端傳入之列, 屬既有行為, 見規則帳本R-OBJ-01之已知限制
|
|
168
|
+
for (let { kind, proc } of getKinds()) {
|
|
169
|
+
for (let [find] of finds) {
|
|
170
|
+
let copy = clone(find)
|
|
171
|
+
await proc('u1', 'tests', 'select', find)
|
|
172
|
+
await proc('u1', 'tests', 'delAll', find)
|
|
173
|
+
same(find, copy, `${kind} find被改動`)
|
|
174
|
+
}
|
|
175
|
+
if (kind === 'none') {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
for (let how of ['insert', 'save', 'mix.add', 'mix.diff']) {
|
|
179
|
+
let input = clone(rows)
|
|
180
|
+
await runWrite(proc, how, input)
|
|
181
|
+
same(input, rows, `${kind} ${how} 寫入列被改動`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('L05 授權函數(addFunCheck)所見之find, 即ORM收到之find(無id數字轉型者)', async function() {
|
|
187
|
+
//R-OBJ-01 依據: S5「授權所見即執行所用」在ORM邊界須成立
|
|
188
|
+
let so = WServOrm(dsStr, WOrmStub, 'u', 'd')
|
|
189
|
+
let seen = []
|
|
190
|
+
so.addFunCheck(({ input }) => {
|
|
191
|
+
seen.push(clone(input))
|
|
192
|
+
})
|
|
193
|
+
await wait(20)
|
|
194
|
+
let list = finds.filter(([, expectCast]) => expectCast === undefined).map(([f]) => f)
|
|
195
|
+
for (let f of list) {
|
|
196
|
+
await so.procOrm('u1', 'tests', 'select', f)
|
|
197
|
+
await so.procOrm('u1', 'tests', 'delAll', f)
|
|
198
|
+
}
|
|
199
|
+
assert.strictEqual(seen.length, calls.length)
|
|
200
|
+
calls.forEach((c, i) => {
|
|
201
|
+
same(c.data, seen[i], `第${i}次ORM收到者與授權函數所見`)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('L06 del與mix之del列含整數length鍵: 不依length迭代, 照常交ORM(數字主鍵轉字串, 其餘原樣)', async function() {
|
|
206
|
+
//R-OBJ-01 規則1、已知限制(del各筆不做__proto__檢查): castIdRows以展開複製, 不依length迭代(行為鎖定, 修前亦綠)
|
|
207
|
+
for (let { kind, cast, proc } of getKinds()) {
|
|
208
|
+
if (kind === 'none') {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
stub.reset()
|
|
212
|
+
await proc('u1', 'tests', 'del', [{ id: 5, length: 3 }, { id: 'a', length: 2 }])
|
|
213
|
+
await proc('u1', 'tests', 'del', { id: 7, length: 0 })
|
|
214
|
+
await proc('u1', 'tests', 'mix', { del: [{ id: 8, length: 1 }] })
|
|
215
|
+
same(calls.map((c) => c.data), cast
|
|
216
|
+
? [[{ id: '5', length: 3 }, { id: 'a', length: 2 }], { id: '7', length: 0 }, [{ id: '8', length: 1 }]]
|
|
217
|
+
: [[{ id: 5, length: 3 }, { id: 'a', length: 2 }], { id: 7, length: 0 }, [{ id: 8, length: 1 }]], kind)
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
|
|
225
|
+
|
|
226
|
+
beforeEach(function() {
|
|
227
|
+
stub.reset()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
let msgFind = (mode) => `invalid find of ${mode}, unsupported key[__proto__]`
|
|
231
|
+
|
|
232
|
+
it('P01 find任何位置含自有__proto__鍵即reject, ORM未被呼叫, 訊息不回顯客戶端之值', async function() {
|
|
233
|
+
//R-OBJ-01 規則3: 頂層、$and/$or/$nor、id之條件物件及其$not、他欄之值、陣列內之物件、深層
|
|
234
|
+
let finds = [
|
|
235
|
+
'{"__proto__":{"x":1}}',
|
|
236
|
+
'{"__proto__":"SECRET-VALUE"}',
|
|
237
|
+
'{"id":"a1","__proto__":{"id":"zzz"}}',
|
|
238
|
+
'{"id":5,"__proto__":1}',
|
|
239
|
+
'{"$and":[{"__proto__":{"x":1}}]}',
|
|
240
|
+
'{"$or":[{"name":"a"},{"$and":[{"__proto__":1}]}]}',
|
|
241
|
+
'{"$nor":[{"__proto__":{}}]}',
|
|
242
|
+
'{"id":{"__proto__":{"$ne":"a"}}}',
|
|
243
|
+
'{"id":{"$in":[1],"__proto__":1}}',
|
|
244
|
+
'{"id":{"$not":{"__proto__":1}}}',
|
|
245
|
+
'{"name":{"__proto__":{"x":1}}}',
|
|
246
|
+
'{"name":{"$in":[{"__proto__":1}]}}',
|
|
247
|
+
'{"a":{"b":{"c":{"__proto__":1}}}}',
|
|
248
|
+
'{"length":1,"__proto__":{"x":1}}',
|
|
249
|
+
]
|
|
250
|
+
for (let { kind, proc } of getKinds()) {
|
|
251
|
+
for (let mode of ['select', 'delAll']) {
|
|
252
|
+
for (let c of finds) {
|
|
253
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, parse(c))))
|
|
254
|
+
assert.strictEqual(err, msgFind(mode), `${kind} ${mode} ${c}`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('P02 寫入列任何深度含自有__proto__鍵: insert、save、mix之add與diff整批reject, ORM未被呼叫', async function() {
|
|
262
|
+
//R-OBJ-01 規則3: 於任何寫入之前, 訊息只列位置; 形狀檢查(R-INPUT-01)優先
|
|
263
|
+
let top = '{"id":"p1","name":"x","__proto__":{"y":1}}'
|
|
264
|
+
let nested = '{"id":"p2","meta":{"__proto__":{"y":1}}}'
|
|
265
|
+
let inArray = '{"id":"p3","list":[{"a":1},{"__proto__":1}]}'
|
|
266
|
+
let good = { id: 'g1', name: 'ok', length: 1 }
|
|
267
|
+
for (let { kind, proc } of getKinds()) {
|
|
268
|
+
if (kind === 'none') {
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
let rj = (mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
|
|
272
|
+
for (let c of [top, nested, inArray]) {
|
|
273
|
+
assert.strictEqual(await rj('insert', [good, parse(c)]), 'failed to insert data, unsupported key[__proto__]: data[1]', `${kind} ${c}`)
|
|
274
|
+
assert.strictEqual(await rj('save', [parse(c), good]), 'failed to save data, unsupported key[__proto__]: data[0]', `${kind} ${c}`)
|
|
275
|
+
assert.strictEqual(await rj('insert', parse(c)), 'failed to insert data, unsupported key[__proto__]: data[0]', `${kind} 單一物件 ${c}`)
|
|
276
|
+
assert.strictEqual(await rj('save', parse(c)), 'failed to save data, unsupported key[__proto__]: data[0]', `${kind} 單一物件 ${c}`)
|
|
277
|
+
assert.strictEqual(await rj('mix', { add: [good, parse(c)] }), 'failed to insert data, unsupported key[__proto__]: data[1]', `${kind} ${c}`)
|
|
278
|
+
assert.strictEqual(await rj('mix', { diff: [parse(c)] }), 'failed to update data, unsupported key[__proto__]: data[0]', `${kind} ${c}`)
|
|
279
|
+
assert.strictEqual(await rj('mix', { add: [parse(c)], diff: [good, parse(c)] }), 'failed to insert data, unsupported key[__proto__]: data[0]; failed to update data, unsupported key[__proto__]: data[1]', `${kind} ${c}`)
|
|
280
|
+
}
|
|
281
|
+
assert.strictEqual(await rj('insert', [parse(top), good, parse(nested), parse(inArray)]), 'failed to insert data, unsupported key[__proto__]: data[0], data[2], data[3]', kind)
|
|
282
|
+
|
|
283
|
+
//形狀檢查優先: 非物件之列先以 not an object 整批拒絕
|
|
284
|
+
assert.strictEqual(await rj('insert', ['abc', parse(top)]), 'failed to insert data, not an object: data[0]', kind)
|
|
285
|
+
|
|
286
|
+
//檢查順序(R-INPUT-01 規則4): __proto__ 檢查早於 R-XSS-01 之主鍵預檢與 R-MIX-01 之跨段同鍵檢查
|
|
287
|
+
assert.strictEqual(await rj('insert', [parse('{"id":"<script>x</script>","__proto__":1}')]), 'failed to insert data, unsupported key[__proto__]: data[0]', kind)
|
|
288
|
+
assert.strictEqual(await rj('mix', { add: [{ id: 's1' }], del: [{ id: 's1' }], diff: [parse('{"id":"d1","__proto__":1}')] }), 'failed to update data, unsupported key[__proto__]: data[0]', kind)
|
|
289
|
+
assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('P03 不在規則內者照常: null原型物件字面量、物件字面量之__proto__設原型之寫入列、mix之del列、他欄之__proto__字串值', async function() {
|
|
294
|
+
//R-OBJ-01 規則3(自有鍵之定義)與已知限制(del各筆交後端)
|
|
295
|
+
for (let { kind, cast, proc } of getKinds()) {
|
|
296
|
+
//{ __proto__: null } 之字面量為null原型物件, 無自有__proto__鍵; 轉型表複製為一般物件, 其餘原物件交ORM
|
|
297
|
+
stub.reset()
|
|
298
|
+
let f = { __proto__: null, id: 'a' }
|
|
299
|
+
await proc('u1', 'tests', 'select', f)
|
|
300
|
+
if (cast) {
|
|
301
|
+
same(calls[0].data, { id: 'a' }, `${kind} null原型之select`)
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
assert.strictEqual(calls[0].data, f, `${kind} 應為同一參考`)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
stub.reset()
|
|
308
|
+
await proc('u1', 'tests', 'select', { name: '__proto__' })
|
|
309
|
+
same(calls[0].data, { name: '__proto__' }, `${kind} 值為字串__proto__之select`)
|
|
310
|
+
|
|
311
|
+
if (kind === 'none') {
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
//寫入列以 { __proto__: x } 設原型(無自有鍵)者: 複製只取自有鍵, 原型不帶入
|
|
315
|
+
stub.reset()
|
|
316
|
+
let r = await proc('u1', 'tests', 'insert', [{ __proto__: { name: 'inherit' }, id: 'q1' }])
|
|
317
|
+
same(calls[0].data, [{ id: 'q1' }], `${kind} 字面量__proto__之insert`)
|
|
318
|
+
same(r, [{ id: 'q1' }], `${kind} 回傳`)
|
|
319
|
+
|
|
320
|
+
stub.reset()
|
|
321
|
+
await proc('u1', 'tests', 'mix', { del: [parse('{"id":"d1","__proto__":{"y":1}}')] })
|
|
322
|
+
assert.strictEqual(calls[0].fn, 'del', `${kind} mix之del列照常交ORM`)
|
|
323
|
+
assert.strictEqual(calls[0].data[0].id, 'd1')
|
|
324
|
+
}
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
//getter, 於o定義可列舉之getter, reads計其被呼叫次數
|
|
328
|
+
let reads = 0
|
|
329
|
+
let getter = (o, k, f) => Object.defineProperty(o, k, {
|
|
330
|
+
enumerable: true,
|
|
331
|
+
configurable: true,
|
|
332
|
+
get() {
|
|
333
|
+
reads++; return f()
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
let boom = (v) => () => {
|
|
337
|
+
throw v
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
it('P04 find與寫入列含存取器屬性(getter、setter)或檢查時拋錯: 整批reject, 不呼叫getter, 不回顯其拋出之值', async function() {
|
|
341
|
+
//R-OBJ-01 規則3(accessor、unreadable): getter每讀一次可能不同, 檢查之值不等於使用之值(可繞過__proto__檢查或改查另一筆), 拋錯則繞過固定訊息並回顯其值(外審 sol F1、F4, opus #2)
|
|
342
|
+
reads = 0
|
|
343
|
+
let revoked = Proxy.revocable({}, {})
|
|
344
|
+
revoked.revoke()
|
|
345
|
+
let finds = [
|
|
346
|
+
['getter', () => getter({}, 'id', () => 'a'), 'unsupported accessor property'],
|
|
347
|
+
['getter拋Error', () => getter({}, 'id', boom(new Error('SECRET-GETTER'))), 'unsupported accessor property'],
|
|
348
|
+
['getter拋字串', () => getter({}, 'id', boom('SECRET-CLIENT-VALUE')), 'unsupported accessor property'],
|
|
349
|
+
['getter於$and內', () => ({ $and: [getter({}, 'name', () => 'x')] }), 'unsupported accessor property'],
|
|
350
|
+
['getter於id條件內', () => ({ id: getter({}, '$in', () => ['a']) }), 'unsupported accessor property'],
|
|
351
|
+
['setter', () => Object.defineProperty({}, 'name', { enumerable: true, set() {} }), 'unsupported accessor property'], // eslint-disable-line accessor-pairs
|
|
352
|
+
['getter回傳含__proto__之物件', () => getter({}, 'name', () => parse('{"__proto__":1}')), 'unsupported accessor property'],
|
|
353
|
+
['已撤銷之Proxy', () => revoked.proxy, 'unreadable value'],
|
|
354
|
+
['ownKeys陷阱拋錯', () => new Proxy({}, {
|
|
355
|
+
ownKeys() {
|
|
356
|
+
throw new Error('SECRET-TRAP')
|
|
357
|
+
}
|
|
358
|
+
}), 'unreadable value'],
|
|
359
|
+
]
|
|
360
|
+
for (let { kind, proc } of getKinds()) {
|
|
361
|
+
for (let mode of ['select', 'delAll']) {
|
|
362
|
+
for (let [label, mk, msg] of finds) {
|
|
363
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, mk())))
|
|
364
|
+
assert.strictEqual(err, `invalid find of ${mode}, ${msg}`, `${kind} ${mode} ${label}`)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
let rows = [
|
|
369
|
+
['getter', () => getter({ id: 'a' }, 'name', () => 'x'), 'unsupported accessor property'],
|
|
370
|
+
['getter拋Error', () => getter({ id: 'a' }, 'name', boom(new Error('SECRET-GETTER'))), 'unsupported accessor property'],
|
|
371
|
+
['getter拋字串', () => getter({ id: 'a' }, 'name', boom('SECRET-CLIENT-VALUE')), 'unsupported accessor property'],
|
|
372
|
+
['巢狀getter', () => ({ id: 'a', meta: getter({}, 'k', () => 1) }), 'unsupported accessor property'],
|
|
373
|
+
['陣列內物件之getter', () => ({ id: 'a', list: [getter({}, 'k', () => 1)] }), 'unsupported accessor property'],
|
|
374
|
+
['getter回傳含__proto__之物件', () => getter({ id: 'a' }, 'meta', () => parse('{"__proto__":1}')), 'unsupported accessor property'],
|
|
375
|
+
['ownKeys陷阱拋錯', () => new Proxy({ id: 'a' }, {
|
|
376
|
+
ownKeys() {
|
|
377
|
+
throw new Error('SECRET-TRAP')
|
|
378
|
+
}
|
|
379
|
+
}), 'unreadable value'],
|
|
380
|
+
]
|
|
381
|
+
for (let { kind, proc } of getKinds()) {
|
|
382
|
+
if (kind === 'none') {
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
let rj = (mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
|
|
386
|
+
for (let [label, mk, msg] of rows) {
|
|
387
|
+
assert.strictEqual(await rj('insert', [mk()]), `failed to insert data, ${msg}: data[0]`, `${kind} insert ${label}`)
|
|
388
|
+
assert.strictEqual(await rj('save', mk()), `failed to save data, ${msg}: data[0]`, `${kind} save ${label}`)
|
|
389
|
+
assert.strictEqual(await rj('mix', { add: [mk()] }), `failed to insert data, ${msg}: data[0]`, `${kind} mix.add ${label}`)
|
|
390
|
+
assert.strictEqual(await rj('mix', { diff: [{ id: 'ok' }, mk()] }), `failed to update data, ${msg}: data[1]`, `${kind} mix.diff ${label}`)
|
|
391
|
+
}
|
|
392
|
+
//原因不同者分組列出
|
|
393
|
+
assert.strictEqual(await rj('insert', [parse('{"id":"p","__proto__":1}'), getter({ id: 'q' }, 'name', () => 'x'), parse('{"id":"r","__proto__":1}')]),
|
|
394
|
+
'failed to insert data, unsupported key[__proto__]: data[0], data[2]; failed to insert data, unsupported accessor property: data[1]', kind)
|
|
395
|
+
}
|
|
396
|
+
assert.strictEqual(reads, 0, 'getter不應被呼叫')
|
|
397
|
+
assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('P05 陣列本身含自有__proto__鍵(不論可否列舉)亦reject; 稀疏陣列之空洞照常', async function() {
|
|
401
|
+
//R-OBJ-01 規則3: 陣列以索引迭代之例外不及於「檢查該陣列本身有無自有__proto__鍵」(外審 sol F3)
|
|
402
|
+
let mkArr = (enumerable) => Object.defineProperty([1, 2], '__proto__', { value: 'SECRET', enumerable })
|
|
403
|
+
for (let { kind, proc } of getKinds()) {
|
|
404
|
+
for (let enumerable of [false, true]) {
|
|
405
|
+
for (let mode of ['select', 'delAll']) {
|
|
406
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, { id: { $in: mkArr(enumerable) } })))
|
|
407
|
+
assert.strictEqual(err, `invalid find of ${mode}, unsupported key[__proto__]`, `${kind} ${mode} ${enumerable}`)
|
|
408
|
+
}
|
|
409
|
+
if (kind !== 'none') {
|
|
410
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', 'insert', { id: 'c', meta: mkArr(enumerable) })))
|
|
411
|
+
assert.strictEqual(err, 'failed to insert data, unsupported key[__proto__]: data[0]', `${kind} insert ${enumerable}`)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
|
|
415
|
+
|
|
416
|
+
//稀疏陣列之空洞: 無屬性描述子, 不視為不支援
|
|
417
|
+
let sparse = [1, , 3] // eslint-disable-line no-sparse-arrays
|
|
418
|
+
await proc('u1', 'tests', 'select', { id: { $in: sparse } })
|
|
419
|
+
assert.strictEqual(calls.length, 1, `${kind} 稀疏陣列之條件照常交ORM`)
|
|
420
|
+
stub.reset()
|
|
421
|
+
}
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('P06 批次容器(insert、save之輸入陣列索引、mix之add/del/diff屬性)為存取器: 整批reject, ORM未被呼叫', async function() {
|
|
425
|
+
//R-OBJ-01 規則3(批次容器): 容器為getter時各檢查各自重讀, 前幾次讀回乾淨列令檢查通過, 其後讀回含__proto__之列或字串(外審 opus 二輪 F1)
|
|
426
|
+
let n
|
|
427
|
+
let mkArr = () => {
|
|
428
|
+
n = 0
|
|
429
|
+
return Object.defineProperty([], 0, {
|
|
430
|
+
enumerable: true,
|
|
431
|
+
get() {
|
|
432
|
+
n++
|
|
433
|
+
return n <= 2 ? { id: 'a', name: 'clean' } : parse('{"id":"a","__proto__":{"polluted":1}}')
|
|
434
|
+
},
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
let mkMix = (key) => {
|
|
438
|
+
n = 0
|
|
439
|
+
return Object.defineProperty({}, key, {
|
|
440
|
+
enumerable: true,
|
|
441
|
+
get() {
|
|
442
|
+
n++
|
|
443
|
+
return n <= 4 ? [{ id: 'ok' }] : [parse('{"id":"zz","__proto__":{"p":1}}'), 'str']
|
|
444
|
+
},
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
for (let { kind, proc } of getKinds()) {
|
|
448
|
+
if (kind === 'none') {
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
451
|
+
let rj = (mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
|
|
452
|
+
assert.strictEqual(await rj('insert', mkArr()), 'failed to insert data, unsupported accessor property: data[0]', `${kind} insert`)
|
|
453
|
+
assert.strictEqual(await rj('save', mkArr()), 'failed to save data, unsupported accessor property: data[0]', `${kind} save`)
|
|
454
|
+
for (let key of ['add', 'del', 'diff']) {
|
|
455
|
+
assert.strictEqual(await rj('mix', mkMix(key)), 'invalid input of mix, unsupported accessor property', `${kind} mix.${key}`)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('P07 偽造Symbol.toStringTag為Array之物件不當陣列走訪: 不呼叫其length getter', async function() {
|
|
462
|
+
//R-OBJ-01 規則3(陣列之判定用Array.isArray): isarr以Object.prototype.toString判定可被偽造, 走訪時讀其length(外審 opus 二輪 F6)
|
|
463
|
+
let reads = 0
|
|
464
|
+
let fake = { [Symbol.toStringTag]: 'Array' }
|
|
465
|
+
Object.defineProperty(fake, 'length', {
|
|
466
|
+
enumerable: true,
|
|
467
|
+
get() {
|
|
468
|
+
reads++
|
|
469
|
+
return 3
|
|
470
|
+
},
|
|
471
|
+
})
|
|
472
|
+
for (let { kind, proc } of getKinds()) {
|
|
473
|
+
for (let mode of ['select', 'delAll']) {
|
|
474
|
+
await proc('u1', 'tests', mode, { name: { $eq: fake } })
|
|
475
|
+
}
|
|
476
|
+
assert.strictEqual(reads, 0, `${kind} length getter不應被呼叫`)
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('K01 欄位值內具整數length鍵之巢狀物件, 其內字串亦須被清除且不新增索引鍵', async function() {
|
|
481
|
+
//R-OBJ-01: wsemi 1.9.17 以前之 clearXSS 以 lodash each 遞迴巢狀物件, {length:3,a:'<script>'} 之 a 不被清除並新增 0、1、2 鍵,
|
|
482
|
+
//length 極大時更於複本建立同數量之屬性而耗盡記憶體(外審 opus #1); wsemi 1.9.18 已修, 本案自此啟用
|
|
483
|
+
let so = WServOrm(dsStr, WOrmStub, 'u', 'd')
|
|
484
|
+
await so.procOrm('u1', 'tests', 'insert', [{ id: 'k1', meta: { length: 3, a: '<script>alert(1)</script>' } }])
|
|
485
|
+
same(calls[0].data[0].meta, { length: 3, a: '<script>alert(1)</script>' }, '巢狀物件之清除')
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
describe('R-INPUT-01 規則3 select與delAll之find須為物件', function() {
|
|
492
|
+
|
|
493
|
+
beforeEach(function() {
|
|
494
|
+
stub.reset()
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
//非條件物件之語料(dspec 一之一第2點: null、陣列、字串、數值、布林、函數、Date、RegExp、Map、Set)
|
|
498
|
+
let bads = [
|
|
499
|
+
['null', null],
|
|
500
|
+
['[]', []],
|
|
501
|
+
['array-string', ['a']],
|
|
502
|
+
['array-object', [{ id: 'a' }]],
|
|
503
|
+
['string', 'a'],
|
|
504
|
+
['empty-string', ''],
|
|
505
|
+
['5', 5],
|
|
506
|
+
['0', 0],
|
|
507
|
+
['NaN', NaN],
|
|
508
|
+
['true', true],
|
|
509
|
+
['false', false],
|
|
510
|
+
['function', () => ({})],
|
|
511
|
+
['Date', new Date(0)],
|
|
512
|
+
['RegExp', /x/],
|
|
513
|
+
['Map', new Map()],
|
|
514
|
+
['Set', new Set()],
|
|
515
|
+
//isobj為真(或可被偽造為真)但非純物件: 自有可列舉鍵可能為空而在轉型表被複製成{}, 令條件消失(select回全表、delAll刪全表), 故一律reject(外審 opus #4、sol F2)
|
|
516
|
+
['類別實例', new (class A {
|
|
517
|
+
constructor() {
|
|
518
|
+
this.name = 'x'
|
|
519
|
+
}
|
|
520
|
+
})()],
|
|
521
|
+
['類別實例(欄位為原型getter)', new (class B {
|
|
522
|
+
get name() {
|
|
523
|
+
return 'x'
|
|
524
|
+
}
|
|
525
|
+
})()],
|
|
526
|
+
['Object.create(proto)', Object.create({ name: 'x' })],
|
|
527
|
+
//原型為「null 原型之一般物件」: 條件繼承自原型,自有鍵為空;「原型之原型為 null」不是根原型之充分條件(外審 opus 二輪 F2,真 lowdb 上 delAll 刪全表)
|
|
528
|
+
['原型為null原型物件之物件', Object.create(Object.assign(Object.create(null), { name: 'alpha' }))],
|
|
529
|
+
['原型為null原型空物件之物件', Object.create(Object.create(null))],
|
|
530
|
+
['原型為null原型物件且帶非Object之constructor', Object.create(Object.assign(Object.create(null), { constructor: class Foo {} }))],
|
|
531
|
+
['偽造Symbol.toStringTag之Map', Object.defineProperty(new Map([['id', 'a']]), Symbol.toStringTag, { value: 'Object' })],
|
|
532
|
+
['偽造Symbol.toStringTag之包裝數值', Object.defineProperty(Object(1), Symbol.toStringTag, { value: 'Object' })],
|
|
533
|
+
['new String', new String('a')], // eslint-disable-line no-new-wrappers
|
|
534
|
+
['Object(1)', Object(1)],
|
|
535
|
+
['arguments', (function() {
|
|
536
|
+
return arguments
|
|
537
|
+
})('a')],
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
it('D01 find給予而非純物件: select與delAll皆整批reject, ORM未被呼叫(四種資料表型態)', async function() {
|
|
541
|
+
//R-INPUT-01 規則3; 檢查早於轉型, 故與isIdCast無關
|
|
542
|
+
for (let { kind, proc } of getKinds()) {
|
|
543
|
+
for (let mode of ['select', 'delAll']) {
|
|
544
|
+
for (let [label, find] of bads) {
|
|
545
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
|
|
546
|
+
assert.strictEqual(err, `invalid find of ${mode}, need an object`, `${kind} ${mode} ${label}`)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
|
|
550
|
+
}
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('D02 find未給、undefined、{}與一般條件: 照常交ORM(無條件), 實參不變', async function() {
|
|
554
|
+
//R-INPUT-01 規則3、dspec 一之一第1點: 三者皆為全部
|
|
555
|
+
for (let { kind, proc } of getKinds()) {
|
|
556
|
+
for (let mode of ['select', 'delAll']) {
|
|
557
|
+
stub.reset()
|
|
558
|
+
await proc('u1', 'tests', mode)
|
|
559
|
+
await proc('u1', 'tests', mode, undefined)
|
|
560
|
+
await proc('u1', 'tests', mode, {})
|
|
561
|
+
await proc('u1', 'tests', mode, { name: 'x' })
|
|
562
|
+
assert.deepStrictEqual(calls.map((c) => c.fn), [mode, mode, mode, mode], kind)
|
|
563
|
+
same(calls.map((c) => c.data), [undefined, undefined, {}, { name: 'x' }], `${kind} ${mode}`)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
it('D04 特殊但合法之純物件照常: null原型、跨realm(vm)、含陣列與巢狀之條件', async function() {
|
|
569
|
+
//R-INPUT-01 規則3(純物件之定義: 原型為null或根原型, 故跨realm之物件通過); 外審 opus 類別3、sol: D01 只測應reject者, 判準改嚴時不會被發現誤擋
|
|
570
|
+
let nullProto = Object.assign(Object.create(null), { name: 'x', length: 1 })
|
|
571
|
+
let crossRealm = vm.runInNewContext('({ name: "x", length: 1, $or: [{ id: "a" }, { name: "b" }] })')
|
|
572
|
+
for (let { kind, cast, proc } of getKinds()) {
|
|
573
|
+
for (let mode of ['select', 'delAll']) {
|
|
574
|
+
for (let [label, find, expected] of [
|
|
575
|
+
['null原型', nullProto, { name: 'x', length: 1 }],
|
|
576
|
+
['跨realm', crossRealm, { name: 'x', length: 1, $or: [{ id: 'a' }, { name: 'b' }] }],
|
|
577
|
+
]) {
|
|
578
|
+
stub.reset()
|
|
579
|
+
await proc('u1', 'tests', mode, find)
|
|
580
|
+
assert.strictEqual(calls.length, 1, `${kind} ${mode} ${label}`)
|
|
581
|
+
if (cast) {
|
|
582
|
+
//轉型表複製: 自有鍵與值相同, 原型為本realm之Object.prototype
|
|
583
|
+
assert.deepStrictEqual(Object.keys(calls[0].data), Object.keys(expected), `${kind} ${mode} ${label} 鍵`)
|
|
584
|
+
assert.strictEqual(Object.getPrototypeOf(calls[0].data), Object.prototype)
|
|
585
|
+
assert.strictEqual(calls[0].data.length, 1)
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
assert.strictEqual(calls[0].data, find, `${kind} ${mode} ${label} 應為同一參考`)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('D03 reject為字串且不回顯客戶端之值; 前處理改成非物件者亦被擋', async function() {
|
|
596
|
+
//R-ERR-01 規則1(字串)、R-MSG-01(訊息不回顯); 檢查作用於前處理之後之input(R-HOOK-01)
|
|
597
|
+
let so = WServOrm(dsStr, WOrmStub, 'u', 'd')
|
|
598
|
+
let err = await quiet(() => getReject(so.procOrm('u1', 'tests', 'select', 'SECRET-VALUE')))
|
|
599
|
+
assert.strictEqual(typeof err, 'string')
|
|
600
|
+
assert.ok(!err.includes('SECRET'), err)
|
|
601
|
+
so.addFunPreProcessing(({ input }) => ({ input: input === undefined ? null : input }))
|
|
602
|
+
await wait(20)
|
|
603
|
+
let err2 = await quiet(() => getReject(so.procOrm('u1', 'tests', 'select')))
|
|
604
|
+
assert.strictEqual(err2, 'invalid find of select, need an object')
|
|
605
|
+
assert.deepStrictEqual(calls, [])
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
describe('R-INPUT-01 規則5 woName須為woItems之自有字串屬性', function() {
|
|
612
|
+
|
|
613
|
+
beforeEach(function() {
|
|
614
|
+
stub.reset()
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
//各入口之最小有效輸入, 令未被woName檢查擋下者必然呼叫ORM
|
|
618
|
+
let modes = [
|
|
619
|
+
['select', undefined],
|
|
620
|
+
['insert', [{ id: 'a' }]],
|
|
621
|
+
['save', [{ id: 'a' }]],
|
|
622
|
+
['del', [{ id: 'a' }]],
|
|
623
|
+
['delAll', undefined],
|
|
624
|
+
['mix', { add: [{ id: 'a' }] }],
|
|
625
|
+
]
|
|
626
|
+
|
|
627
|
+
it('W01 woName為原型鏈上之名稱、路徑字串或非字串: 一律以固定字串reject, ORM未被呼叫', async function() {
|
|
628
|
+
//R-INPUT-01 規則5、R-ERR-01 規則1: 原 lodash get 取到 Object.prototype 之成員或解析路徑(tests.select 取到 ORM 之函數), 以 TypeError 物件 reject
|
|
629
|
+
let names = ['__proto__', 'constructor', 'toString', 'hasOwnProperty', 'valueOf', 'isPrototypeOf', 'tests.select', 'tests["select"]', 'tests.keys', 'tests.', 'Tests', 'nonexistent', '']
|
|
630
|
+
let nonStrings = [1, ['tests'], { a: 1 }, null, undefined, true, Symbol('tests')]
|
|
631
|
+
for (let { kind, proc } of getKinds()) {
|
|
632
|
+
for (let [mode, input] of modes) {
|
|
633
|
+
for (let name of names) {
|
|
634
|
+
let err = await quiet(() => getReject(proc('u1', name, mode, input)))
|
|
635
|
+
assert.strictEqual(err, `can not find table[${name}] in ORM`, `${kind} ${mode} ${name}`)
|
|
636
|
+
}
|
|
637
|
+
for (let name of nonStrings) {
|
|
638
|
+
let err = await quiet(() => getReject(proc('u1', name, mode, input)))
|
|
639
|
+
assert.strictEqual(err, 'can not find table[] in ORM', `${kind} ${mode} ${String(name)}`)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
it('W02 自有字串屬性照常: ds之鍵含「.」或為Object.prototype成員之名稱者亦然(不是名稱黑名單)', async function() {
|
|
647
|
+
//R-INPUT-01 規則5: 判準是自有屬性, 不是名稱黑名單
|
|
648
|
+
let ds = {
|
|
649
|
+
'a.b': { keys: ['id', 'name'], settings: { id: { pk: true, type: 'STRING' }, name: { type: 'STRING' } } },
|
|
650
|
+
'constructor': { keys: ['id', 'name'], settings: { id: { pk: true, type: 'STRING' }, name: { type: 'STRING' } } },
|
|
651
|
+
'tests': dsStr.tests,
|
|
652
|
+
}
|
|
653
|
+
let so = WServOrm(ds, WOrmStub, 'u', 'd')
|
|
654
|
+
for (let name of ['a.b', 'constructor', 'tests']) {
|
|
655
|
+
stub.reset()
|
|
656
|
+
let r = await so.procOrm('u1', name, 'insert', [{ id: 5, name: 'x' }])
|
|
657
|
+
assert.deepStrictEqual(r, [{ id: '5' }], name)
|
|
658
|
+
await so.procOrm('u1', name, 'select', { id: 5 })
|
|
659
|
+
assert.deepStrictEqual(calls.map((c) => c.fn), ['insert', 'select'], name)
|
|
660
|
+
same(calls[1].data, { id: '5' }, `${name} 轉型(isIdCast 取到該表之 ds)`)
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
describe('R-OBJ-01 規則5 find之巢狀深度上限', function() {
|
|
668
|
+
|
|
669
|
+
beforeEach(function() {
|
|
670
|
+
stub.reset()
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
//builders, 容器 = 一般物件與陣列
|
|
674
|
+
//eqObj(n): n 個巢狀物件容器; andChain(n): n 層 $and(根物件 + 每層一個陣列與一個物件, 共 1+2n 個容器); notChain(n): { id: { $not: … } } 共 n+2 個容器(含根)
|
|
675
|
+
let eqObj = (n) => {
|
|
676
|
+
let o = { k: 1 }
|
|
677
|
+
for (let i = 1; i < n; i++) {
|
|
678
|
+
o = { k: o }
|
|
679
|
+
}
|
|
680
|
+
return o
|
|
681
|
+
}
|
|
682
|
+
let andChain = (n) => {
|
|
683
|
+
let o = { id: 1 }
|
|
684
|
+
for (let i = 0; i < n; i++) {
|
|
685
|
+
o = { $and: [o] }
|
|
686
|
+
}
|
|
687
|
+
return o
|
|
688
|
+
}
|
|
689
|
+
let notChain = (n) => {
|
|
690
|
+
let c = { $eq: 1 }
|
|
691
|
+
for (let i = 0; i < n; i++) {
|
|
692
|
+
c = { $not: c }
|
|
693
|
+
}
|
|
694
|
+
return { id: c }
|
|
695
|
+
}
|
|
696
|
+
//findDepth(d): 恰 d 個容器層之 find(d>=2)
|
|
697
|
+
let findDepth = (d) => ({ name: eqObj(d - 1) })
|
|
698
|
+
|
|
699
|
+
it('T01 find之容器層數超過64即以固定字串reject, 64層以內照常; 深層巢狀不再以RangeError物件reject', async function() {
|
|
700
|
+
//R-OBJ-01 規則5、R-ERR-01 規則1: 修前字串型主鍵表約1173層$and、約1857層$not鏈起以RangeError物件reject, 其餘表型態原樣交後端
|
|
701
|
+
for (let { kind, proc } of getKinds()) {
|
|
702
|
+
for (let mode of ['select', 'delAll']) {
|
|
703
|
+
//邊界: 64 層照常、65 層 reject
|
|
704
|
+
for (let [label, find, ok] of [
|
|
705
|
+
['eq 64', findDepth(64), true],
|
|
706
|
+
['eq 65', findDepth(65), false],
|
|
707
|
+
['$and 31層(63容器)', andChain(31), true],
|
|
708
|
+
['$and 32層(65容器)', andChain(32), false],
|
|
709
|
+
['$not 鏈 62(64容器)', notChain(62), true],
|
|
710
|
+
['$not 鏈 63(65容器)', notChain(63), false],
|
|
711
|
+
]) {
|
|
712
|
+
stub.reset()
|
|
713
|
+
if (ok) {
|
|
714
|
+
await proc('u1', 'tests', mode, find)
|
|
715
|
+
assert.strictEqual(calls.length, 1, `${kind} ${mode} ${label} 應交ORM`)
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
|
|
719
|
+
assert.strictEqual(err, `invalid find of ${mode}, too deep`, `${kind} ${mode} ${label}`)
|
|
720
|
+
assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
//極深: 皆為固定字串(typeof string), 不洩漏 RangeError 物件
|
|
724
|
+
stub.reset()
|
|
725
|
+
for (let [label, find] of [['$and 1e4', andChain(1e4)], ['$not 1e4', notChain(1e4)], ['eq 1e5', findDepth(1e5)], ['$and 1e5', andChain(1e5)]]) {
|
|
726
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
|
|
727
|
+
assert.strictEqual(err, `invalid find of ${mode}, too deep`, `${kind} ${mode} ${label}`)
|
|
728
|
+
}
|
|
729
|
+
assert.strictEqual(calls.length, 0, `${kind} ${mode} ORM不應被呼叫`)
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
it('T02 合理之複雜查詢照常; 寫入列之深層巢狀由adjustData轉為固定字串(行為鎖定, 不設上限)', async function() {
|
|
735
|
+
//R-OBJ-01 規則5: 合理查詢最深約7個容器; 列之遞迴由wsemi clearXSS進行, 約1e4層起拋RangeError, 被adjustData之try/catch轉為 failed to adjust data
|
|
736
|
+
let find = { $or: [{ id: { $in: [1, 2] } }, { $and: [{ name: 'x' }, { $nor: [{ name: 'y' }, { id: { $not: { $eq: 3 } } }] }] }] }
|
|
737
|
+
for (let { kind, proc } of getKinds()) {
|
|
738
|
+
for (let mode of ['select', 'delAll']) {
|
|
739
|
+
stub.reset()
|
|
740
|
+
await proc('u1', 'tests', mode, find)
|
|
741
|
+
assert.strictEqual(calls.length, 1, `${kind} ${mode}`)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
let row = (n) => ({ id: 'r1', meta: eqObj(n) })
|
|
745
|
+
let arr = (n) => {
|
|
746
|
+
let a = []
|
|
747
|
+
for (let i = 0; i < n; i++) {
|
|
748
|
+
a = [a]
|
|
749
|
+
}
|
|
750
|
+
return { id: 'r1', meta: a }
|
|
751
|
+
}
|
|
752
|
+
for (let { kind, proc } of getKinds()) {
|
|
753
|
+
if (kind === 'none') {
|
|
754
|
+
continue
|
|
755
|
+
}
|
|
756
|
+
stub.reset()
|
|
757
|
+
//深層列之結果依wsemi之遞迴與呼叫堆疊而定(預設堆疊: 以固定字串 failed to adjust data reject; 加大堆疊: 可能交給後端而resolve),
|
|
758
|
+
//故只斷言「不以Error物件reject」, 不鎖定哪一種(外審 opus 二輪 F11)
|
|
759
|
+
let outcome = (mode, input) => quiet(() => proc('u1', 'tests', mode, input)).then(() => 'resolved', (e) => e)
|
|
760
|
+
for (let n of [1e4, 1e5]) {
|
|
761
|
+
for (let [label, mode, input] of [['insert', 'insert', [row(n)]], ['save', 'save', arr(n)], ['mix.add', 'mix', { add: [row(n)] }], ['mix.diff', 'mix', { diff: [arr(n)] }]]) {
|
|
762
|
+
let r = await outcome(mode, input)
|
|
763
|
+
assert.ok(r === 'resolved' || typeof r === 'string', `${kind} ${label} ${n}: ${r instanceof Error ? r.constructor.name : typeof r}`)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
describe('R-ERR-01 規則5 邊界保證: mapOrm 之非字串例外一律轉為固定字串', function() {
|
|
773
|
+
|
|
774
|
+
beforeEach(function() {
|
|
775
|
+
stub.reset()
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
let FIXED = 'failed to process input'
|
|
779
|
+
|
|
780
|
+
it('C01 mode為物件、Symbol或null原型物件: 以固定字串reject(原 invalid mode 之字串轉換拋TypeError)', async function() {
|
|
781
|
+
//R-ERR-01 規則5(外審 opus 二輪 F8): 與T5同類, 使用端值直接進模板字串
|
|
782
|
+
for (let { kind, proc } of getKinds()) {
|
|
783
|
+
for (let mode of [parse('{"toString":1}'), Symbol('x'), Object.create(null)]) {
|
|
784
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, {})))
|
|
785
|
+
assert.strictEqual(err, FIXED, `${kind} ${String(typeof mode)}`)
|
|
786
|
+
}
|
|
787
|
+
//對照: 可轉字串者維持既有之 invalid mode 訊息
|
|
788
|
+
assert.strictEqual(await quiet(() => getReject(proc('u1', 'tests', 'nope', {}))), 'invalid mode: nope', kind)
|
|
789
|
+
}
|
|
790
|
+
assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
it('C02 寫入列為已撤銷之Proxy或get陷阱拋錯之Proxy: 以固定字串reject, 不轉出拋出之值', async function() {
|
|
794
|
+
//R-ERR-01 規則5(外審 opus 二輪 F3): isobj 讀 Symbol.toStringTag 即拋錯, 在 getReasonBadKey 之 try 之外
|
|
795
|
+
let revoked = Proxy.revocable({ id: 'x' }, {})
|
|
796
|
+
revoked.revoke()
|
|
797
|
+
let mkTrap = () => new Proxy({ id: 'x' }, {
|
|
798
|
+
get() {
|
|
799
|
+
throw new Error('SECRET-TRAP')
|
|
800
|
+
},
|
|
801
|
+
})
|
|
802
|
+
for (let { kind, proc } of getKinds()) {
|
|
803
|
+
if (kind === 'none') {
|
|
804
|
+
continue
|
|
805
|
+
}
|
|
806
|
+
let rj = (mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
|
|
807
|
+
for (let mk of [() => revoked.proxy, mkTrap]) {
|
|
808
|
+
assert.strictEqual(await rj('insert', [mk()]), FIXED, `${kind} insert`)
|
|
809
|
+
assert.strictEqual(await rj('insert', mk()), FIXED, `${kind} insert 單一`)
|
|
810
|
+
assert.strictEqual(await rj('save', [mk()]), FIXED, `${kind} save`)
|
|
811
|
+
assert.strictEqual(await rj('mix', { add: [mk()] }), FIXED, `${kind} mix.add`)
|
|
812
|
+
assert.strictEqual(await rj('mix', { diff: [mk()] }), FIXED, `${kind} mix.diff`)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('C03 被拒之find帶util.inspect.custom拋錯: 仍回特定之固定訊息(console.log 不得使 reject 變成例外物件)', async function() {
|
|
819
|
+
//R-ERR-01 規則5(外審 opus 二輪 F7): console.log 原 input 時呼叫使用端之 inspect.custom
|
|
820
|
+
//不用 quiet(其以空函數取代 console.log, 不會格式化引數而測不出): 改以 util.format 格式化引數(同真 console.log)後丟棄
|
|
821
|
+
let logOrig = console.log
|
|
822
|
+
console.log = (...a) => {
|
|
823
|
+
util.format(...a)
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
826
|
+
for (let { kind, proc } of getKinds()) {
|
|
827
|
+
for (let mode of ['select', 'delAll']) {
|
|
828
|
+
let f = parse('{"__proto__":1}')
|
|
829
|
+
f[Symbol.for('nodejs.util.inspect.custom')] = () => {
|
|
830
|
+
throw new Error('SECRET-INSPECT')
|
|
831
|
+
}
|
|
832
|
+
let err = await getReject(proc('u1', 'tests', mode, f))
|
|
833
|
+
assert.strictEqual(err, `invalid find of ${mode}, unsupported key[__proto__]`, `${kind} ${mode}`)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
finally {
|
|
838
|
+
console.log = logOrig
|
|
839
|
+
}
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
it('C04 循環或鏈狀共用子結構之find使遞迴爆堆疊: 以固定字串reject(字串型主鍵表; 其餘表型態原樣交ORM)', async function() {
|
|
843
|
+
//R-ERR-01 規則5、R-OBJ-01 已知限制(外審 opus 二輪 F4): 深度檢查不涵蓋共用子結構, 由邊界保證兜底
|
|
844
|
+
let cyc = { $and: [] }
|
|
845
|
+
cyc.$and.push(cyc)
|
|
846
|
+
let shared = { id: 1 }
|
|
847
|
+
let chain = []
|
|
848
|
+
for (let i = 0; i < 5000; i++) {
|
|
849
|
+
shared = { $and: [shared] }
|
|
850
|
+
chain.push(shared)
|
|
851
|
+
}
|
|
852
|
+
//由短到長依序被 getReasonBadKey 彈出(堆疊後進先出, 故陣列內長者排前): 每個 $and 節點皆經根之 $or 以第 3 層先被走到, 檢查所見最大深度僅 5, 實際最長路徑約 1e4 個容器
|
|
853
|
+
let dag = { $or: [...chain].reverse() }
|
|
854
|
+
for (let { kind, cast, proc } of getKinds()) {
|
|
855
|
+
for (let mode of ['select', 'delAll']) {
|
|
856
|
+
for (let [label, find] of [['循環', cyc], ['鏈狀共用', dag]]) {
|
|
857
|
+
stub.reset()
|
|
858
|
+
if (cast) {
|
|
859
|
+
let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
|
|
860
|
+
assert.strictEqual(err, FIXED, `${kind} ${mode} ${label}`)
|
|
861
|
+
assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
await proc('u1', 'tests', mode, find)
|
|
865
|
+
assert.strictEqual(calls.length, 1, `${kind} ${mode} ${label} 原樣交ORM`)
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
it('C05 woItems 之值非 ORM(行程內被改)者: 以固定字串reject', async function() {
|
|
873
|
+
//R-ERR-01 規則5(外審 opus 二輪 F10b): getOwnItem 只排除 null/undefined, 其後 wo.select is not a function 原以 TypeError 物件 reject
|
|
874
|
+
let so = WServOrm(dsStr, WOrmStub, 'u', 'd')
|
|
875
|
+
so.woItems.bad0 = 0
|
|
876
|
+
so.woItems.bad1 = {}
|
|
877
|
+
for (let name of ['bad0', 'bad1']) {
|
|
878
|
+
for (let [mode, input] of [['select', {}], ['delAll', {}], ['del', [{ id: 'a' }]]]) {
|
|
879
|
+
let err = await quiet(() => getReject(so.procOrm('u1', name, mode, input)))
|
|
880
|
+
assert.strictEqual(err, FIXED, `${name} ${mode}`)
|
|
881
|
+
}
|
|
882
|
+
//insert 於 ORM 呼叫前先經 adjustData(ds 無該表而拋錯), 由其 try/catch 轉為既有之固定字串
|
|
883
|
+
assert.strictEqual(await quiet(() => getReject(so.procOrm('u1', name, 'insert', [{ id: 'a' }]))), 'failed to adjust data', `${name} insert`)
|
|
884
|
+
}
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
describe('R-INPUT-01 規則5 getWoItems 以自有屬性建表', function() {
|
|
891
|
+
|
|
892
|
+
beforeEach(function() {
|
|
893
|
+
stub.reset()
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
it('W03 ds之鍵為__proto__(JSON載入之schema): 該表為woItems之自有屬性且可用, woItems之原型不被改設', async function() {
|
|
897
|
+
//R-INPUT-01 規則5、R-OBJ-01 規則2(外審 opus 二輪 F10a): 原 woItems[v]=wo 改設 woItems 之原型, 該表永遠 can not find table
|
|
898
|
+
let ds = JSON.parse('{"__proto__":{"keys":["id","name"],"settings":{"id":{"pk":true,"type":"STRING"}}},"tests":{"keys":["id","name"],"settings":{"id":{"pk":true,"type":"STRING"}}}}')
|
|
899
|
+
let so = WServOrm(ds, WOrmStub, 'u', 'd')
|
|
900
|
+
assert.strictEqual(Object.getPrototypeOf(so.woItems), Object.prototype)
|
|
901
|
+
assert.deepStrictEqual(Object.keys(so.woItems).sort(), ['__proto__', 'tests'])
|
|
902
|
+
await so.procOrm('u1', '__proto__', 'select', { name: 'x' })
|
|
903
|
+
await so.procOrm('u1', 'tests', 'select', { name: 'x' })
|
|
904
|
+
assert.strictEqual(calls.length, 2)
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
})
|