w-serv-orm 1.1.15 → 1.1.17

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.
@@ -375,11 +375,13 @@ describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
375
375
  ['巢狀getter', () => ({ id: 'a', meta: getter({}, 'k', () => 1) }), 'unsupported accessor property'],
376
376
  ['陣列內物件之getter', () => ({ id: 'a', list: [getter({}, 'k', () => 1)] }), 'unsupported accessor property'],
377
377
  ['getter回傳含__proto__之物件', () => getter({ id: 'a' }, 'meta', () => parse('{"__proto__":1}')), 'unsupported accessor property'],
378
+ //規則7 ②(第82項)起寫入列頂層之Proxy一律 irregular reject 且不觸發任何陷阱(原 unreadable value, 陷阱被執行後才拋錯); 以 reads 斷言陷阱0次
378
379
  ['ownKeys陷阱拋錯', () => new Proxy({ id: 'a' }, {
379
380
  ownKeys() {
381
+ reads++
380
382
  throw new Error('SECRET-TRAP')
381
383
  }
382
- }), 'unreadable value'],
384
+ }), 'unsupported irregular object or array'],
383
385
  ]
384
386
  for (let { kind, proc } of getKinds()) {
385
387
  if (kind === 'none') {
@@ -1389,28 +1391,34 @@ describe('R-ERR-01 規則5 邊界保證: mapOrm 之非字串例外一律轉為
1389
1391
  assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
1390
1392
  })
1391
1393
 
1392
- it('C02 寫入列為已撤銷之Proxy或get陷阱拋錯之Proxy: 以固定字串reject, 不轉出拋出之值', async function() {
1394
+ it('C02 寫入列為已撤銷之Proxy或get陷阱拋錯之Proxy: 以固定字串reject, 不轉出拋出之值, 陷阱不被執行', async function() {
1393
1395
  //R-ERR-01 規則5(外審 opus 二輪 F3): isobj 讀 Symbol.toStringTag 即拋錯, 在 getReasonBadKey 之 try 之外
1396
+ //規則7 ②(第82項)起: 頂層列為Proxy(含已撤銷者)以 types.isProxy 判斷(不觸發陷阱), 一律 irregular reject(原為邊界保證之 failed to process input, 且 get 陷阱先被執行而拋錯)
1397
+ let trapReads = 0
1394
1398
  let revoked = Proxy.revocable({ id: 'x' }, {})
1395
1399
  revoked.revoke()
1396
1400
  let mkTrap = () => new Proxy({ id: 'x' }, {
1397
1401
  get() {
1402
+ trapReads++
1398
1403
  throw new Error('SECRET-TRAP')
1399
1404
  },
1400
1405
  })
1406
+ let IRREG = 'unsupported irregular object or array'
1401
1407
  for (let { kind, proc } of getKinds()) {
1402
1408
  if (kind === 'none') {
1403
1409
  continue
1404
1410
  }
1405
1411
  let rj = (mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
1406
- for (let mk of [() => revoked.proxy, mkTrap]) {
1407
- assert.strictEqual(await rj('insert', [mk()]), FIXED, `${kind} insert`)
1408
- assert.strictEqual(await rj('insert', mk()), FIXED, `${kind} insert 單一`)
1409
- assert.strictEqual(await rj('save', [mk()]), FIXED, `${kind} save`)
1410
- assert.strictEqual(await rj('mix', { add: [mk()] }), FIXED, `${kind} mix.add`)
1411
- assert.strictEqual(await rj('mix', { diff: [mk()] }), FIXED, `${kind} mix.diff`)
1412
+ //單一輸入為已撤銷之Proxy時無法分類為陣列或單一列(Array.isArray拋TypeError), 於讀取與ORM呼叫之前以容器層之固定訊息拒收(無位置); 未撤銷者仍為列層之 data[0]
1413
+ for (let [mk, single] of [[() => revoked.proxy, `failed to insert data, ${IRREG}`], [mkTrap, `failed to insert data, ${IRREG}: data[0]`]]) {
1414
+ assert.strictEqual(await rj('insert', [mk()]), `failed to insert data, ${IRREG}: data[0]`, `${kind} insert`)
1415
+ assert.strictEqual(await rj('insert', mk()), single, `${kind} insert 單一`)
1416
+ assert.strictEqual(await rj('save', [mk()]), `failed to save data, ${IRREG}: data[0]`, `${kind} save`)
1417
+ assert.strictEqual(await rj('mix', { add: [mk()] }), `failed to insert data, ${IRREG}: data[0]`, `${kind} mix.add`)
1418
+ assert.strictEqual(await rj('mix', { diff: [mk()] }), `failed to update data, ${IRREG}: data[0]`, `${kind} mix.diff`)
1412
1419
  }
1413
1420
  }
1421
+ assert.strictEqual(trapReads, 0, '陷阱不應被執行')
1414
1422
  assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
1415
1423
  })
1416
1424
 
@@ -1438,8 +1446,9 @@ describe('R-ERR-01 規則5 邊界保證: mapOrm 之非字串例外一律轉為
1438
1446
  }
1439
1447
  })
1440
1448
 
1441
- it('C04 循環或鏈狀共用子結構之find使遞迴爆堆疊: 以固定字串reject(循環: 字串型主鍵表, 其餘表型態原樣交ORM; 鏈狀共用: 所有表型態皆由規則6之品牌網reject)', async function() {
1442
- //R-ERR-01 規則5、R-OBJ-01 已知限制(外審 opus 二輪 F4): 深度檢查不涵蓋共用子結構, 由邊界保證兜底
1449
+ it('C04 循環或鏈狀共用子結構之find(原使遞迴爆堆疊): 所有表型態皆以固定字串 too deep reject, ORM未被呼叫', async function() {
1450
+ //R-ERR-01 規則5、R-OBJ-01 規則5續(第82項): 原深度檢查不涵蓋共用子結構, 循環於字串型主鍵表由邊界保證轉為 failed to process input、其餘表型態原樣交ORM,
1451
+ //鏈狀共用由規則6之品牌網以 irregular reject; 現由 measureFind 於所有表型態判定最長路徑(含循環), 預期隨之更新(只改訊息預期與循環之非字串型表, 不放寬)
1443
1452
  let cyc = { $and: [] }
1444
1453
  cyc.$and.push(cyc)
1445
1454
  let shared = { id: 1 }
@@ -1450,25 +1459,14 @@ describe('R-ERR-01 規則5 邊界保證: mapOrm 之非字串例外一律轉為
1450
1459
  }
1451
1460
  //由短到長依序被 getReasonBadKey 彈出(堆疊後進先出, 故陣列內長者排前): 每個 $and 節點皆經根之 $or 以第 3 層先被走到, 檢查所見最大深度僅 5, 實際最長路徑約 1e4 個容器
1452
1461
  let dag = { $or: [...chain].reverse() }
1453
- for (let { kind, cast, proc } of getKinds()) {
1462
+ for (let { kind, proc } of getKinds()) {
1454
1463
  for (let mode of ['select', 'delAll']) {
1455
1464
  for (let [label, find] of [['循環', cyc], ['鏈狀共用', dag]]) {
1456
1465
  stub.reset()
1457
- if (label === '鏈狀共用') {
1458
- //規則6: 最長路徑約1e4個容器, 遠超結構化複製之遞迴上限(Node 24.19.0 實測約1576個容器, 與冷熱無關), 品牌網於複製前即以固定字串reject, 所有資料表型態皆然
1459
- let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
1460
- assert.strictEqual(err, `invalid find of ${mode}, unsupported irregular object or array`, `${kind} ${mode} ${label}`)
1461
- assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)
1462
- }
1463
- else if (cast) {
1464
- let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
1465
- assert.strictEqual(err, FIXED, `${kind} ${mode} ${label}`)
1466
- assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)
1467
- }
1468
- else {
1469
- await proc('u1', 'tests', mode, find)
1470
- assert.strictEqual(calls.length, 1, `${kind} ${mode} ${label} 原樣交ORM`)
1471
- }
1466
+ //最長路徑約1e4個容器(鏈狀共用)或無限(循環), 皆超過64個容器層: measureFind 於結構化複製與castIdFind之前以固定字串reject, 所有資料表型態皆然
1467
+ let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
1468
+ assert.strictEqual(err, `invalid find of ${mode}, too deep`, `${kind} ${mode} ${label}`)
1469
+ assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)
1472
1470
  }
1473
1471
  }
1474
1472
  }
@@ -1510,3 +1508,1501 @@ describe('R-INPUT-01 規則5 getWoItems 以自有屬性建表', function() {
1510
1508
  })
1511
1509
 
1512
1510
  })
1511
+
1512
+
1513
+ //R-OBJ-01 規則7(第82項, Argon 外審第三輪 B-02 與核實時另發現之 F5、F6): 寫入列與批次容器——檢查之值即使用之值, 且不執行使用端程式
1514
+ //預言機為手寫預期(依規則7各句), 不取自被測函式之回傳; 僅替身ORM(接真ORM見 api-objcopy A27、A28)
1515
+ describe('R-OBJ-01 規則7 寫入列之id只認自有可列舉資料屬性、頂層列之乾淨判準(S01、S02)', function() {
1516
+
1517
+ beforeEach(function() {
1518
+ stub.reset()
1519
+ })
1520
+
1521
+ let hide = (o, k, v) => Object.defineProperty(o, k, { value: v, enumerable: false, configurable: true, writable: true })
1522
+ let KINDS = ['str', 'unknown', 'int'] //寫入與del之路徑不含 none(ds無該表, 寫入於adjustData拋錯)
1523
+ let ENTRIES = ['insert', 'save', 'mix.add', 'mix.diff', 'del', 'mix.del']
1524
+ let msgWrite = { 'insert': 'failed to insert data', 'save': 'failed to save data', 'mix.add': 'failed to insert data', 'mix.diff': 'failed to update data' }
1525
+ let run = (proc, entry, row) => {
1526
+ switch (entry) {
1527
+ case 'insert': return proc('u1', 'tests', 'insert', [row])
1528
+ case 'save': return proc('u1', 'tests', 'save', [row])
1529
+ case 'mix.add': return proc('u1', 'tests', 'mix', { add: [row] })
1530
+ case 'mix.diff': return proc('u1', 'tests', 'mix', { diff: [row] })
1531
+ case 'del': return proc('u1', 'tests', 'del', [row])
1532
+ case 'mix.del': return proc('u1', 'tests', 'mix', { del: [row] })
1533
+ }
1534
+ throw new Error(`entry ${entry}`)
1535
+ }
1536
+ let firstRow = () => {
1537
+ let d = calls[0].data
1538
+ return Array.isArray(d) ? d[0] : d
1539
+ }
1540
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
1541
+
1542
+ //id形態表: w=寫入列(insert、save、mix.add、mix.diff), d=del, md=mix.del 之預期
1543
+ // w: 'keep'=ORM收到之自有id同送入者; 'cast'=字串型/未知型表轉為字串(整數型表原數字); 'gen'=視為未提供(字串型/未知型表補主鍵, 整數型表無自有id); 'rej:<原因>'=整批reject
1544
+ // d: 'ref'=ORM收到之列即送入之同一物件(本層不轉型亦不複製); 'cast'=字串型/未知型表複製並轉為字串, 整數型表同ref; null=不測(Proxy之del列交後端, 屬既有)
1545
+ // md: 'ok'=通過且本層不轉型(同d之'ref'); 'cast'=通過且字串型/未知型表轉字串(同d之'cast'); 'rej'=整批reject
1546
+ let forms = () => [
1547
+ { k: 'F01', label: '自有可列舉字串id', mk: () => ({ row: { id: 'a1', name: 'x' } }), w: 'keep', d: 'ref', md: 'ok' },
1548
+ { k: 'F02', label: '自有可列舉有限數字id', mk: () => ({ row: { id: 123, name: 'x' } }), w: 'cast', d: 'cast', md: 'cast' },
1549
+ { k: 'F04', label: '自有不可列舉字串id', mk: () => ({ row: hide({ name: 'x' }, 'id', 'a1') }), w: 'gen', d: 'ref', md: 'rej' },
1550
+ { k: 'F05', label: '自有不可列舉數字id(修前被「復活」為可列舉字串id)', mk: () => ({ row: hide({ name: 'x' }, 'id', 123) }), w: 'gen', d: 'ref', md: 'rej' },
1551
+ { k: 'F06', label: '繼承字串id(Object.create)', mk: () => ({ row: Object.assign(Object.create({ id: 'a1' }), { name: 'x' }) }), w: 'gen', d: 'ref', md: 'rej' },
1552
+ { k: 'F07', label: '繼承數字id(修前被「復活」為可列舉字串id)', mk: () => ({ row: Object.assign(Object.create({ id: 123 }), { name: 'x' }) }), w: 'gen', d: 'ref', md: 'rej' },
1553
+ {
1554
+ k: 'F08',
1555
+ label: '自有可列舉存取器id',
1556
+ mk: () => {
1557
+ let c = { n: 0 }
1558
+ let row = { name: 'x' }
1559
+ Object.defineProperty(row, 'id', {
1560
+ enumerable: true,
1561
+ get() {
1562
+ c.n++; return 'a1'
1563
+ }
1564
+ })
1565
+ return { row, c }
1566
+ },
1567
+ w: 'rej:unsupported accessor property',
1568
+ d: 'ref',
1569
+ md: 'rej'
1570
+ },
1571
+ {
1572
+ k: 'F09',
1573
+ label: '自有不可列舉存取器id',
1574
+ mk: () => {
1575
+ let c = { n: 0 }
1576
+ let row = { name: 'x' }
1577
+ Object.defineProperty(row, 'id', {
1578
+ enumerable: false,
1579
+ get() {
1580
+ c.n++; return 'a1'
1581
+ }
1582
+ })
1583
+ return { row, c }
1584
+ },
1585
+ w: 'gen',
1586
+ d: 'ref',
1587
+ md: 'rej'
1588
+ },
1589
+ {
1590
+ k: 'F10',
1591
+ label: '繼承存取器id(類別getter)',
1592
+ mk: () => {
1593
+ let c = { n: 0 }
1594
+ class R {
1595
+ constructor() {
1596
+ this.name = 'x'
1597
+ }
1598
+
1599
+ get id() {
1600
+ c.n++; return 'a1'
1601
+ }
1602
+ }
1603
+ return { row: new R(), c }
1604
+ },
1605
+ w: 'gen',
1606
+ d: 'ref',
1607
+ md: 'rej'
1608
+ },
1609
+ { k: 'F11', label: 'null原型列、自有id', mk: () => ({ row: Object.assign(Object.create(null), { id: 'a1', name: 'x' }) }), w: 'keep', d: 'ref', md: 'ok' },
1610
+ {
1611
+ k: 'F12',
1612
+ label: 'Proxy列(計數陷阱)',
1613
+ mk: () => {
1614
+ let c = { n: 0 }
1615
+ let row = new Proxy({ id: 'a1', name: 'x' }, {
1616
+ get(t, k, r) {
1617
+ c.n++; return Reflect.get(t, k, r)
1618
+ },
1619
+ getOwnPropertyDescriptor(t, k) {
1620
+ c.n++; return Reflect.getOwnPropertyDescriptor(t, k)
1621
+ },
1622
+ ownKeys(t) {
1623
+ c.n++; return Reflect.ownKeys(t)
1624
+ },
1625
+ has(t, k) {
1626
+ c.n++; return Reflect.has(t, k)
1627
+ },
1628
+ })
1629
+ return { row, c }
1630
+ },
1631
+ w: 'rej:unsupported irregular object or array',
1632
+ d: null,
1633
+ md: 'rej'
1634
+ },
1635
+ { k: 'F13', label: 'null原型之Map(品牌物件, 無標籤可辨)', mk: () => ({ row: Object.setPrototypeOf(new Map([['id', 'a1']]), null) }), w: 'rej:unsupported irregular object or array', d: 'ref', md: 'ok' },
1636
+ { k: 'F14', label: '列自有Symbol.toStringTag', mk: () => ({ row: { id: 'a1', name: 'x', [Symbol.toStringTag]: 'X' } }), w: 'rej:unsupported irregular object or array', d: 'ref', md: 'rej' },
1637
+ ]
1638
+
1639
+ it('S01 列之id形態 x 六個入口 x 三種資料表型態: id只認自有可列舉資料屬性, getter與陷阱0次, 不改呼叫端物件(B-02 ①②④、F6)', async function() {
1640
+ //規則7 ①②; R-ID-02 規則4續; R-MIX-01 規則6
1641
+ let done = 0
1642
+ let fs0 = forms()
1643
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
1644
+ let cast = kind !== 'int'
1645
+ for (let f of fs0) {
1646
+ for (let entry of ENTRIES) {
1647
+ stub.reset()
1648
+ let { row, c } = f.mk()
1649
+ let label = `${kind} ${f.k}(${f.label}) ${entry}`
1650
+ let isW = entry.startsWith('insert') || entry === 'save' || entry.startsWith('mix.a') || entry === 'mix.diff'
1651
+ let exp = isW ? f.w : entry === 'del' ? f.d : f.md
1652
+ if (exp === null) {
1653
+ continue //Proxy之del列交後端(R-INPUT-01 規則2), 屬既有行為, 不測
1654
+ }
1655
+ let before = Reflect.ownKeys(row).length
1656
+ if (c) {
1657
+ c.n = 0 //上一行對Proxy取ownKeys是測試自己的讀取, 不計入
1658
+ }
1659
+ if (typeof exp === 'string' && exp.startsWith('rej')) {
1660
+ let msg = isW ? `${msgWrite[entry]}, ${exp.slice(4)}: data[0]` : exp === 'rej' ? 'failed to delete data, unsupported irregular object or array: data[0]' : null
1661
+ let err = await quiet(() => getReject(run(proc, entry, row)))
1662
+ assert.strictEqual(err, msg, label)
1663
+ assert.deepStrictEqual(calls, [], `${label} ORM不應被呼叫`)
1664
+ }
1665
+ else {
1666
+ await quiet(() => run(proc, entry, row))
1667
+ assert.strictEqual(calls.length, 1, `${label} ORM被呼叫一次`)
1668
+ let got = firstRow()
1669
+ if (isW) {
1670
+ let want = { keep: 'a1', cast: cast ? '123' : 123 }[exp]
1671
+ if (exp === 'gen') {
1672
+ if (cast) {
1673
+ assert.ok(isGen(got.id), `${label} 補主鍵, 實收 ${String(got.id)}`)
1674
+ }
1675
+ else {
1676
+ assert.ok(!hasOwn(got, 'id'), `${label} 整數型主鍵表不補值`)
1677
+ }
1678
+ }
1679
+ else {
1680
+ assert.strictEqual(got.id, want, `${label} id`)
1681
+ }
1682
+ assert.strictEqual(Object.getPrototypeOf(got), Object.prototype, `${label} 寫入複本為一般物件`)
1683
+ assert.ok(got !== row, `${label} 寫入複本非呼叫端物件`)
1684
+ assert.strictEqual(got.name, 'x', `${label} 他欄不變`)
1685
+ }
1686
+ else if (exp === 'cast' && cast) {
1687
+ assert.ok(got !== row, `${label} 複製`)
1688
+ assert.strictEqual(got.id, '123', `${label} 轉字串`)
1689
+ assert.strictEqual(row.id, 123, `${label} 不改呼叫端物件`)
1690
+ }
1691
+ else {
1692
+ assert.ok(got === row, `${label} 本層不轉型亦不複製(同一參考)`)
1693
+ }
1694
+ }
1695
+ if (c) {
1696
+ assert.strictEqual(c.n, 0, `${label} getter/陷阱次數`)
1697
+ }
1698
+ assert.strictEqual(Reflect.ownKeys(row).length, before, `${label} 不改呼叫端物件`)
1699
+ done++
1700
+ }
1701
+ }
1702
+ }
1703
+ assert.strictEqual(done, 3 * (13 * 6 - 1), '完成數(3表型態 x 13形態 x 6入口, 扣掉Proxy之del列)')
1704
+ })
1705
+
1706
+ it('S01b 存取器id於檢查通過後才被執行並向列加入含自有__proto__之欄位(B-02 ②原重現): getter不被執行, 該欄位不被寫入', async function() {
1707
+ //規則7 ①②; 修前: 字串型表getter被執行2次(mix 3次)、整數型表1次(mix 2次), 列內meta含自有__proto__被送到ORM
1708
+ let mkOwn = (c) => {
1709
+ let r = { name: 'n3' }
1710
+ Object.defineProperty(r, 'id', {
1711
+ enumerable: false,
1712
+ get() {
1713
+ c.n++; r.meta = parse('{"__proto__":{"polluted":1}}'); return 'a3'
1714
+ }
1715
+ })
1716
+ return r
1717
+ }
1718
+ let mkInherited = (c) => {
1719
+ class R {
1720
+ constructor() {
1721
+ this.name = 'n3'
1722
+ }
1723
+
1724
+ get id() {
1725
+ c.n++; this.meta = parse('{"__proto__":{"polluted":1}}'); return 'a3'
1726
+ }
1727
+ }
1728
+ return new R()
1729
+ }
1730
+ let done = 0
1731
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
1732
+ for (let [lab, mk] of [['自有不可列舉getter', mkOwn], ['繼承getter', mkInherited]]) {
1733
+ for (let entry of ['insert', 'save', 'mix.add', 'mix.diff']) {
1734
+ stub.reset()
1735
+ let c = { n: 0 }
1736
+ let row = mk(c)
1737
+ await quiet(() => run(proc, entry, row))
1738
+ assert.strictEqual(c.n, 0, `${kind} ${lab} ${entry} getter不被執行`)
1739
+ assert.strictEqual(calls.length, 1, `${kind} ${lab} ${entry}`)
1740
+ assert.ok(!hasOwn(firstRow(), 'meta'), `${kind} ${lab} ${entry} getter內加入之欄位不被寫入`)
1741
+ done++
1742
+ }
1743
+ }
1744
+ }
1745
+ assert.strictEqual(done, 3 * 2 * 4, '完成數')
1746
+ })
1747
+
1748
+ it('S02 頂層列為Proxy、帶標籤、內建品牌物件(原型改為null): 整批reject並列位置, 陷阱0次, 與其他原因分組; 類別實例DTO、Object.create、跨realm、null原型之列照收(Q1(a))', async function() {
1749
+ //規則7 ②及其與find(strict)之差異; 修前: null原型之Map等通過並被mapOwn洗成{}而寫入垃圾列, Proxy列之陷阱被執行14~17次
1750
+ let nul = (o) => Object.setPrototypeOf(o, null)
1751
+ let c = { n: 0 }
1752
+ let mkProxy = () => new Proxy({ id: 'p', name: 'x' }, {
1753
+ get(t, k, r) {
1754
+ c.n++; return Reflect.get(t, k, r)
1755
+ },
1756
+ getOwnPropertyDescriptor(t, k) {
1757
+ c.n++; return Reflect.getOwnPropertyDescriptor(t, k)
1758
+ },
1759
+ ownKeys(t) {
1760
+ c.n++; return Reflect.ownKeys(t)
1761
+ },
1762
+ has(t, k) {
1763
+ c.n++; return Reflect.has(t, k)
1764
+ },
1765
+ getPrototypeOf(t) {
1766
+ c.n++; return Reflect.getPrototypeOf(t)
1767
+ },
1768
+ })
1769
+ let bads = [
1770
+ ['Proxy', mkProxy],
1771
+ ['自有標籤', () => ({ id: 'p', [Symbol.toStringTag]: 'X' })],
1772
+ ['原型上之標籤', () => Object.assign(Object.create(Object.assign(Object.create(null), { [Symbol.toStringTag]: 'X' })), { id: 'p' })],
1773
+ ['Map(null原型)', () => nul(new Map([['id', 'p']]))],
1774
+ ['Set(null原型)', () => nul(new Set([1]))],
1775
+ ['WeakMap(null原型)', () => nul(new WeakMap())],
1776
+ ['Promise(null原型)', () => nul(Promise.resolve(1))],
1777
+ ['ArrayBuffer(null原型)', () => nul(new ArrayBuffer(2))],
1778
+ ['DataView(null原型)', () => nul(new DataView(new ArrayBuffer(2)))],
1779
+ ['Uint8Array(null原型)', () => nul(new Uint8Array(2))],
1780
+ ]
1781
+ let done = 0
1782
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
1783
+ for (let [lab, mk] of bads) {
1784
+ let L = `${kind} ${lab}`
1785
+ let rj = (mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
1786
+ for (let [mode, mk2, msg] of [
1787
+ ['insert', (r) => [r], 'failed to insert data'],
1788
+ ['save', (r) => [r], 'failed to save data'],
1789
+ ['insert', (r) => r, 'failed to insert data'], //單一物件輸入
1790
+ ['mix', (r) => ({ add: [r] }), 'failed to insert data'],
1791
+ ['mix', (r) => ({ diff: [{ id: 'ok' }, r] }), 'failed to update data'],
1792
+ ]) {
1793
+ stub.reset()
1794
+ c.n = 0
1795
+ let idx = mode === 'mix' && msg === 'failed to update data' ? 1 : 0
1796
+ assert.strictEqual(await rj(mode, mk2(mk())), `${msg}, unsupported irregular object or array: data[${idx}]`, `${L} ${mode} ${msg}`)
1797
+ assert.strictEqual(c.n, 0, `${L} 陷阱次數`)
1798
+ assert.deepStrictEqual(calls, [], `${L} ORM不應被呼叫`)
1799
+ done++
1800
+ }
1801
+ //多筆批次中之第2筆, 與其他原因分組(依首次出現之順序)
1802
+ assert.strictEqual(await rj('insert', [{ id: 'ok' }, mk()]), 'failed to insert data, unsupported irregular object or array: data[1]', `${L} 第2筆`)
1803
+ assert.strictEqual(await rj('insert', [mk(), parse('{"id":"q","__proto__":1}')]),
1804
+ 'failed to insert data, unsupported irregular object or array: data[0]; failed to insert data, unsupported key[__proto__]: data[1]', `${L} 分組`)
1805
+ done++
1806
+ }
1807
+ }
1808
+ assert.strictEqual(done, 3 * 10 * 6, '完成數')
1809
+
1810
+ //差異(Q1(a)): 這些列照收, ORM收到自有可列舉鍵之複本(mapOwn), 與直呼後端一致
1811
+ class Dto {
1812
+ constructor() {
1813
+ this.id = 'c1'
1814
+ this.name = 'n'
1815
+ }
1816
+
1817
+ method() {}
1818
+ }
1819
+ let vmObj = vm.runInNewContext('({ id: "v1", name: "n" })')
1820
+ let accepts = [
1821
+ ['類別實例DTO(含自有欄位)', () => new Dto(), { id: 'c1', name: 'n' }],
1822
+ ['Object.create(proto)含自有欄位', () => Object.assign(Object.create({ inherited: 1 }), { id: 'o1', name: 'n' }), { id: 'o1', name: 'n' }],
1823
+ ['跨realm(vm)之列', () => vmObj, { id: 'v1', name: 'n' }],
1824
+ ['null原型之列', () => Object.assign(Object.create(null), { id: 'u1', name: 'n' }), { id: 'u1', name: 'n' }],
1825
+ ['含Symbol鍵與不可列舉鍵之列(後端亦略過)', () => hide(Object.assign({ id: 's1', name: 'n' }, { [Symbol('s')]: 1 }), 'hid', 1), { id: 's1', name: 'n' }],
1826
+ ]
1827
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
1828
+ for (let [lab, mk, expected] of accepts) {
1829
+ stub.reset()
1830
+ await quiet(() => proc('u1', 'tests', 'insert', [mk()]))
1831
+ assert.strictEqual(calls.length, 1, `${kind} ${lab}`)
1832
+ let got = firstRow()
1833
+ assert.deepStrictEqual(Object.keys(got).sort(), Object.keys(expected).sort(), `${kind} ${lab} 鍵`)
1834
+ assert.strictEqual(got.id, expected.id, `${kind} ${lab} id`)
1835
+ assert.strictEqual(Object.getPrototypeOf(got), Object.prototype, `${kind} ${lab} 複本為本realm一般物件`)
1836
+ }
1837
+ }
1838
+ })
1839
+
1840
+ })
1841
+
1842
+
1843
+ describe('R-OBJ-01 規則7 批次容器(S03、S04)', function() {
1844
+
1845
+ beforeEach(function() {
1846
+ stub.reset()
1847
+ })
1848
+
1849
+ let KINDS = ['str', 'unknown', 'int']
1850
+ let evil = () => parse('{"id":"zz","name":"e","meta":{"__proto__":{"polluted":1}}}') //列內含巢狀自有__proto__: 規則3須擋下
1851
+ let okRow = () => ({ id: 'a1', name: 'x' })
1852
+ let sp = (n) => new Array(n)
1853
+ let rj = (proc, mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
1854
+ let elapsed = async (fn) => {
1855
+ let t = Date.now()
1856
+ let r = await fn()
1857
+ return [r, Date.now() - t]
1858
+ }
1859
+
1860
+ it('S03 批次容器為偽造標籤之類陣列、Proxy陣列、有空洞(含原型帶資料索引)、原型鏈有標籤: 整批reject, 繞過規則3之列不被寫入, length與陷阱0次(F5)', async function() {
1861
+ //規則7 ③④; R-INPUT-01 規則1、2(批次容器); 修前: 偽造標籤與空洞+繼承索引兩形狀使含巢狀自有__proto__之列通過getMsgsBadKey並被送到ORM
1862
+ let reads = 0
1863
+ let counted = (target) => new Proxy(target, {
1864
+ get(t, k, r) {
1865
+ reads++; return Reflect.get(t, k, r)
1866
+ },
1867
+ has(t, k) {
1868
+ reads++; return Reflect.has(t, k)
1869
+ },
1870
+ ownKeys(t) {
1871
+ reads++; return Reflect.ownKeys(t)
1872
+ },
1873
+ getOwnPropertyDescriptor(t, k) {
1874
+ reads++; return Reflect.getOwnPropertyDescriptor(t, k)
1875
+ },
1876
+ })
1877
+ let fakeTag = () => ({
1878
+ [Symbol.toStringTag]: 'Array',
1879
+ get length() {
1880
+ reads++; return 1
1881
+ },
1882
+ 0: evil(),
1883
+ })
1884
+ let holeInherited = () => Object.setPrototypeOf(new Array(1), { 0: evil() })
1885
+ //[標籤, 容器工廠, 寫入/del之訊息尾(含位置), mix段之訊息尾, 僅限insert與save(fakeTag於mix回既有之need..., 於del為單一列交後端)]
1886
+ let containers = [
1887
+ ['偽造標籤之類陣列', fakeTag, null],
1888
+ ['Proxy陣列', () => counted([okRow(), okRow()]), 'unsupported irregular object or array'],
1889
+ ['中間有空洞', () => [okRow(), , okRow()], 'unsupported sparse array: data[1]'], // eslint-disable-line no-sparse-arrays
1890
+ ['全為空洞', () => sp(3), 'unsupported sparse array: data[0]'],
1891
+ ['空洞且原型帶資料索引', holeInherited, 'unsupported sparse array: data[0]'],
1892
+ ['原型鏈有標籤之陣列', () => Object.setPrototypeOf([okRow()], Object.assign(Object.create(Array.prototype), { [Symbol.toStringTag]: 'X' })), 'unsupported irregular object or array'],
1893
+ //索引為存取器(可列舉或不可列舉): 其後各步驟以array[index]讀取會執行使用端程式並可每次回傳不同列(外審 sol F01); 由getErrBatch於讀取任何元素之前以描述子拒收, getter 0次
1894
+ ['索引為可列舉存取器', () => Object.defineProperty([okRow()], 0, {
1895
+ enumerable: true,
1896
+ get() {
1897
+ reads++; return okRow()
1898
+ }
1899
+ }), 'unsupported accessor property: data[0]'],
1900
+ ['索引為不可列舉存取器', () => Object.defineProperty([okRow()], 0, {
1901
+ enumerable: false,
1902
+ get() {
1903
+ reads++; return okRow()
1904
+ }
1905
+ }), 'unsupported accessor property: data[0]'],
1906
+ //已撤銷之Proxy: Array.isArray拋TypeError, 無法分類為陣列或單一列, 於任何讀取與ORM呼叫之前固定拒收(外審 sol F06; mix之形狀檢查視為非陣列)
1907
+ ['已撤銷之Proxy陣列', () => {
1908
+ let rv = Proxy.revocable([okRow()], {}); rv.revoke(); return rv.proxy
1909
+ }, 'unsupported irregular object or array', 'need'],
1910
+ ]
1911
+ let done = 0
1912
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
1913
+ for (let [lab, mk, tail, mixNeed] of containers) {
1914
+ let L = `${kind} ${lab}`
1915
+ reads = 0
1916
+ //insert、save
1917
+ for (let [mode, msg] of [['insert', 'failed to insert data'], ['save', 'failed to save data']]) {
1918
+ stub.reset()
1919
+ let err = await rj(proc, mode, mk())
1920
+ //偽造標籤者被視為單一列, 由頂層列檢查以irregular回報(其自有標籤使其不乾淨)
1921
+ assert.strictEqual(err, tail === null ? `${msg}, unsupported irregular object or array: data[0]` : `${msg}, ${tail}`, `${L} ${mode}`)
1922
+ assert.deepStrictEqual(calls, [], `${L} ${mode} ORM不應被呼叫`)
1923
+ done++
1924
+ }
1925
+ //del: 容器檢查適用; 偽造標籤者不被當陣列(原 isarr 為真而依其length迭代), 視為單一列交後端, length getter 0次
1926
+ stub.reset()
1927
+ if (tail !== null) {
1928
+ assert.strictEqual(await rj(proc, 'del', mk()), `failed to delete data, ${tail}`, `${L} del`)
1929
+ assert.deepStrictEqual(calls, [], `${L} del ORM不應被呼叫`)
1930
+ }
1931
+ else {
1932
+ let fake = mk()
1933
+ await quiet(() => proc('u1', 'tests', 'del', fake))
1934
+ assert.strictEqual(calls.length, 1, `${L} del 交後端一次`)
1935
+ assert.ok(calls[0].data === fake, `${L} del 以單一列(同一物件)交後端, 不展開為陣列`)
1936
+ }
1937
+ done++
1938
+ //mix: add、del、diff 各為批次容器
1939
+ for (let [key, msg] of [['add', 'failed to insert data'], ['del', 'failed to delete data'], ['diff', 'failed to update data']]) {
1940
+ stub.reset()
1941
+ let err = await rj(proc, 'mix', { [key]: mk() })
1942
+ assert.strictEqual(err, tail === null || mixNeed === 'need' ? 'invalid input of mix, need the result object of ltdtDiffByKey' : `${msg}, ${tail}`, `${L} mix.${key}`)
1943
+ assert.deepStrictEqual(calls, [], `${L} mix.${key} ORM不應被呼叫`)
1944
+ done++
1945
+ }
1946
+ assert.strictEqual(reads, 0, `${L} length getter與陷阱不被執行`)
1947
+ }
1948
+ //多段並列時以 '; ' 串接(add、del、diff之序)
1949
+ stub.reset()
1950
+ assert.strictEqual(await rj(proc, 'mix', { add: sp(2), del: [{ id: 'x' }], diff: [okRow(), , okRow()] }), // eslint-disable-line no-sparse-arrays
1951
+ 'failed to insert data, unsupported sparse array: data[0]; failed to update data, unsupported sparse array: data[1]', `${kind} mix多段`)
1952
+ assert.deepStrictEqual(calls, [], `${kind} mix多段 ORM不應被呼叫`)
1953
+ //對照(規則3不退化): 同一個含巢狀自有__proto__之列放在一般陣列, 仍以既有訊息reject
1954
+ assert.strictEqual(await rj(proc, 'insert', [evil()]), 'failed to insert data, unsupported key[__proto__]: data[0]', `${kind} 對照`)
1955
+ //對照: 一般之密陣列與空陣列照常
1956
+ await quiet(() => proc('u1', 'tests', 'insert', [okRow(), okRow()]))
1957
+ assert.strictEqual(calls.length, 1, `${kind} 一般密陣列照常`)
1958
+ stub.reset()
1959
+ await quiet(() => proc('u1', 'tests', 'insert', []))
1960
+ assert.strictEqual(calls.length, 1, `${kind} 空陣列照舊交ORM(R-INPUT-01 規則1)`)
1961
+ }
1962
+ assert.strictEqual(done, 3 * ((9 * 2) + 9 + (9 * 3)), '完成數(3表型態 x (9容器 x insert/save + 9 x del + 9 x mix三段))')
1963
+ })
1964
+
1965
+ it('S04 稀疏巨陣列(列內巢狀、批次容器 x 六個入口、find之$in): 立即reject, 不逐索引迴圈、不配置同數量之物件, ORM未被呼叫(A-07 ③)', async function() {
1966
+ //規則7 ③⑤; 修前: 列內巢狀 1e8 個空洞15.8秒才resolve、2^32-1逾40秒未返回; 批次容器 1e7 個空洞配置606MB, 1e8 OOM
1967
+ //有限大小先斷言(1e5): 若檢查被拿掉, 於此即失敗而不致掛住, 再跑極大值
1968
+ let build = (len) => [
1969
+ //[標籤, 入口, 輸入, 預期]
1970
+ ['列內巢狀(陣列列)', 'insert', [{ id: 'a1', arr: sp(len) }], 'failed to insert data, unsupported sparse array: data[0]'],
1971
+ ['列內巢狀(單一物件)', 'insert', { id: 'a1', arr: sp(len) }, 'failed to insert data, unsupported sparse array: data[0]'],
1972
+ ['列內巢狀(save第2列)', 'save', [okRow(), { id: 'a2', deep: { arr: sp(len) } }], 'failed to save data, unsupported sparse array: data[1]'],
1973
+ ['列內巢狀(第0個有值)', 'insert', [{ id: 'a1', arr: Object.assign(sp(len), { 0: 'x' }) }], 'failed to insert data, unsupported sparse array: data[0]'],
1974
+ ['mix.add列內巢狀', 'mix', { add: [{ id: 'a1', arr: sp(len) }] }, 'failed to insert data, unsupported sparse array: data[0]'],
1975
+ ['mix.diff列內巢狀', 'mix', { diff: [{ id: 'a1', arr: sp(len) }] }, 'failed to update data, unsupported sparse array: data[0]'],
1976
+ ['容器 insert', 'insert', sp(len), 'failed to insert data, unsupported sparse array: data[0]'],
1977
+ ['容器 save', 'save', sp(len), 'failed to save data, unsupported sparse array: data[0]'],
1978
+ ['容器 del', 'del', sp(len), 'failed to delete data, unsupported sparse array: data[0]'],
1979
+ ['容器 mix.add', 'mix', { add: sp(len) }, 'failed to insert data, unsupported sparse array: data[0]'],
1980
+ ['容器 mix.del', 'mix', { del: sp(len) }, 'failed to delete data, unsupported sparse array: data[0]'],
1981
+ ['容器 mix.diff', 'mix', { diff: sp(len) }, 'failed to update data, unsupported sparse array: data[0]'],
1982
+ ]
1983
+ let findCases = (len) => [
1984
+ ['find $in', { id: { $in: sp(len) } }],
1985
+ ['find 非id欄位$in', { name: { $in: sp(len) } }],
1986
+ ['find 第0個有值', { id: { $in: Object.assign(sp(len), { 0: 'x' }) } }],
1987
+ ]
1988
+ let done = 0
1989
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
1990
+ for (let len of [1e5, 2 ** 32 - 1, 1e9]) {
1991
+ for (let [lab, mode, input, expected] of build(len)) {
1992
+ stub.reset()
1993
+ let [err, ms] = await elapsed(() => rj(proc, mode, input))
1994
+ assert.strictEqual(err, expected, `${kind} ${lab} len=${len}`)
1995
+ assert.ok(ms < 1000, `${kind} ${lab} len=${len} 耗時 ${ms} ms`)
1996
+ assert.deepStrictEqual(calls, [], `${kind} ${lab} len=${len} ORM不應被呼叫`)
1997
+ done++
1998
+ }
1999
+ for (let mode of ['select', 'delAll']) {
2000
+ for (let [lab, find] of findCases(len)) {
2001
+ stub.reset()
2002
+ let [err, ms] = await elapsed(() => rj(proc, mode, find))
2003
+ assert.strictEqual(err, `invalid find of ${mode}, unsupported irregular object or array`, `${kind} ${mode} ${lab} len=${len}`)
2004
+ assert.ok(ms < 1000, `${kind} ${mode} ${lab} len=${len} 耗時 ${ms} ms`)
2005
+ assert.deepStrictEqual(calls, [], `${kind} ${mode} ${lab} len=${len} ORM不應被呼叫`)
2006
+ done++
2007
+ }
2008
+ }
2009
+ }
2010
+ }
2011
+ assert.strictEqual(done, 3 * 3 * (12 + 2 * 3), '完成數(3表型態 x 3長度 x (12 + 2 x 3))')
2012
+ })
2013
+
2014
+ })
2015
+
2016
+
2017
+ describe('R-MIX-01 規則6 mix之同鍵比對所見之id即實際寫入或刪除所用之id(S05)', function() {
2018
+
2019
+ beforeEach(function() {
2020
+ stub.reset()
2021
+ })
2022
+
2023
+ let KINDS = ['str', 'unknown', 'int']
2024
+ let hide = (o, k, v) => Object.defineProperty(o, k, { value: v, enumerable: false, configurable: true, writable: true })
2025
+ let rj = (proc, input) => quiet(() => getReject(proc('u1', 'tests', 'mix', input)))
2026
+ let msgDelIrregular = (ks) => `failed to delete data, unsupported irregular object or array: ${ks.map((k) => `data[${k}]`).join(', ')}`
2027
+
2028
+ it('S05 不可列舉或繼承之id不參與同鍵比對(原誤拒), 自有可列舉之同鍵仍reject; mix.del列之id為存取器、繼承、Proxy、帶標籤者整批reject(F6)', async function() {
2029
+ //規則: R-MIX-01 規則1、6; 修前 add 列 id 為不可列舉 'a1' 且 del 列 id 'a1' 被誤拒為 same id(寫入者實為補值之主鍵); mix.del 列 id 為 getter 時比對所見與後端所用不同
2030
+ let done = 0
2031
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
2032
+ let cast = kind !== 'int'
2033
+ //不再誤拒: add/diff 列之 id 非自有可列舉 → 視為未提供(補主鍵), del 照常
2034
+ for (let [lab, mkRow] of [
2035
+ ['自有不可列舉', () => hide({ name: 'new' }, 'id', 'a1')],
2036
+ ['繼承', () => Object.assign(Object.create({ id: 'a1' }), { name: 'new' })],
2037
+ ]) {
2038
+ for (let [seg, mkInput, order] of [
2039
+ ['add', (r) => ({ add: [r], del: [{ id: 'a1' }] }), ['insert', 'del']],
2040
+ ['diff', (r) => ({ del: [{ id: 'a1' }], diff: [r] }), ['del', 'save']],
2041
+ ]) {
2042
+ stub.reset()
2043
+ await quiet(() => proc('u1', 'tests', 'mix', mkInput(mkRow())))
2044
+ assert.deepStrictEqual(calls.map((v) => v.fn), order, `${kind} ${lab} ${seg} 呼叫序`)
2045
+ let written = calls.find((v) => v.fn !== 'del').data[0]
2046
+ assert.strictEqual(written.name, 'new', `${kind} ${lab} ${seg}`)
2047
+ if (cast) {
2048
+ assert.ok(isGen(written.id), `${kind} ${lab} ${seg} 補主鍵, 實收 ${String(written.id)}`)
2049
+ }
2050
+ else {
2051
+ assert.ok(!Object.prototype.hasOwnProperty.call(written, 'id'), `${kind} ${lab} ${seg} 整數型表不補值`)
2052
+ }
2053
+ same(calls.find((v) => v.fn === 'del').data, [{ id: 'a1' }], `${kind} ${lab} ${seg} del`)
2054
+ done++
2055
+ }
2056
+ }
2057
+ //數字之不可列舉id: 同樣不參與比對
2058
+ stub.reset()
2059
+ await quiet(() => proc('u1', 'tests', 'mix', { add: [hide({ name: 'new' }, 'id', 5)], del: [{ id: 5 }] }))
2060
+ assert.deepStrictEqual(calls.map((v) => v.fn), ['insert', 'del'], `${kind} 數字`)
2061
+ assert.strictEqual(calls[1].data[0].id, cast ? '5' : 5, `${kind} 數字 del`)
2062
+ done++
2063
+ //對照(R-MIX-01 不退化): 自有可列舉之同鍵仍整批reject, 不寫入任何一筆
2064
+ stub.reset()
2065
+ assert.strictEqual(await rj(proc, { add: [{ id: 'a1', name: 'new' }], del: [{ id: 'a1' }] }), 'invalid input of mix, same id in add and del: add[0], del[0]', `${kind} 同鍵`)
2066
+ if (cast) {
2067
+ assert.strictEqual(await rj(proc, { add: [{ id: 5 }], del: [{ id: '5' }] }), 'invalid input of mix, same id in add and del: add[0], del[0]', `${kind} 數字與字串同鍵(R-ID-02)`)
2068
+ }
2069
+ assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
2070
+ done++
2071
+
2072
+ //mix.del 列: 存取器、繼承、Proxy、帶標籤 → 整批reject並列位置; getter與陷阱0次
2073
+ let c = { n: 0 }
2074
+ let withGetter = (enumerable) => {
2075
+ let r = {}
2076
+ Object.defineProperty(r, 'id', {
2077
+ enumerable,
2078
+ get() {
2079
+ c.n++; return 'a7'
2080
+ }
2081
+ })
2082
+ return r
2083
+ }
2084
+ class Inh {
2085
+ get id() {
2086
+ c.n++
2087
+ return 'a7'
2088
+ }
2089
+ }
2090
+ let mkProxy = () => new Proxy({ id: 'a7' }, {
2091
+ get(t, k, r) {
2092
+ c.n++; return Reflect.get(t, k, r)
2093
+ },
2094
+ getOwnPropertyDescriptor(t, k) {
2095
+ c.n++; return Reflect.getOwnPropertyDescriptor(t, k)
2096
+ }
2097
+ })
2098
+ let dels = [
2099
+ { id: 'ok1' }, //0
2100
+ withGetter(true), //1 自有可列舉存取器
2101
+ { id: 'ok2' }, //2
2102
+ Object.create({ id: 'a7' }), //3 繼承資料屬性
2103
+ withGetter(false), //4 自有不可列舉存取器
2104
+ new Inh(), //5 繼承存取器
2105
+ mkProxy(), //6 Proxy
2106
+ { id: 'a7', [Symbol.toStringTag]: 'X' }, //7 帶標籤
2107
+ hide({}, 'id', 'ok3'), //8 自有不可列舉資料屬性: 拒收(各後端讀法未必相同, 以同鍵防線之前提為重, fail-closed)
2108
+ Object.assign(Object.create(null), { id: 'ok4' }), //9 null原型
2109
+ ]
2110
+ stub.reset()
2111
+ assert.strictEqual(await rj(proc, { add: [{ id: 'a7', name: 'added' }], del: dels }), msgDelIrregular([1, 3, 4, 5, 6, 7, 8]), `${kind} mix.del 多筆`)
2112
+ assert.strictEqual(c.n, 0, `${kind} getter與陷阱不被執行`)
2113
+ assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫(修前: add之a7新增後隨即被後端依getter所讀之a7刪除而resolve)`)
2114
+ done++
2115
+ //僅合法列: 照常, 且add與del之同鍵比對仍運作
2116
+ stub.reset()
2117
+ await quiet(() => proc('u1', 'tests', 'mix', { add: [{ id: 'n1' }], del: [dels[0], dels[2], dels[9]] }))
2118
+ assert.deepStrictEqual(calls.map((v) => v.fn), ['insert', 'del'], `${kind} 合法mix.del`)
2119
+ done++
2120
+ }
2121
+ assert.strictEqual(done, 3 * (4 + 1 + 1 + 1 + 1), '完成數')
2122
+ })
2123
+
2124
+ })
2125
+
2126
+
2127
+ describe('R-OBJ-01 規則5 續 find之最長路徑深度與展開後容器數(S06、S07)', function() {
2128
+
2129
+ beforeEach(function() {
2130
+ stub.reset()
2131
+ })
2132
+
2133
+ let MAX_DEPTH = 64 //容器層, 根為第1層
2134
+ let MAX_NODES = 100000 //展開後容器數(共用子結構依出現次數重複計)
2135
+ let deep = (mode) => `invalid find of ${mode}, too deep`
2136
+ let large = (mode) => `invalid find of ${mode}, too large`
2137
+ let rj = (proc, mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
2138
+ //m個巢狀容器之鏈: {a:{a:...{id:'a1'}}}
2139
+ let chainObj = (m) => {
2140
+ let x = { id: 'a1' }
2141
+ for (let i = 1; i < m; i++) {
2142
+ x = { a: x }
2143
+ }
2144
+ return x
2145
+ }
2146
+ //共用鏈S之DAG: 根之最長路徑為 m+2 個容器.三種鍵序,使「首次到達之走訪」(getReasonBadKey,堆疊後進先出,後面的鍵先被走訪)與「最長路徑之走訪」(measureFind,鍵序先到先走)各異:
2147
+ // 'q-first': {q:{r:S}, p:S} getReasonBadKey 先走p(S在第2層, 修前之首次到達深度僅 1+m 而通過); measureFind 先走q(深路徑先展開, 由堆疊深度抓到)
2148
+ // 'p-first': {p:S, q:{r:S}} getReasonBadKey 先走q(深路徑先到達, 由首次到達之深度檢查抓到)
2149
+ // 'tri': {a:S, b:{r:S}, c:S} 兩個走訪都先經淺路徑展開S,深路徑最後才重用已完成之S: 只有 measureFind 之共用節點檢查(或完成時檢查)抓得到
2150
+ let dagRoot = (m, order) => {
2151
+ let s = chainObj(m)
2152
+ return order === 'q-first' ? { q: { r: s }, p: s } : order === 'p-first' ? { p: s, q: { r: s } } : { a: s, b: { r: s }, c: s }
2153
+ }
2154
+ //指數型DAG: x_{i+1}={$and:[x_i,x_i]}, 展開後物件數 2^(k+1)-1(含陣列之容器數為 3*2^k-2), 相異容器 2k+1, 最長路徑 2k+1
2155
+ let expDag = (k) => {
2156
+ let x = { id: 'a1' }
2157
+ for (let i = 0; i < k; i++) {
2158
+ x = { $and: [x, x] }
2159
+ }
2160
+ return x
2161
+ }
2162
+
2163
+ it('S06 共用子結構: 最長路徑恰64個容器通過、65個reject(含首次到達較淺之DAG); 展開後容器數恰100,000通過、100,001reject; 循環reject; 合理重用照常(A-07 ①②)', async function() {
2164
+ //規則5續; 修前: 首次到達深度僅1+m, 最長路徑65~403個容器之DAG通過; 展開數無上限, 指數型DAG展開2^k個物件
2165
+ let done = 0
2166
+ for (let { kind, proc } of getKinds()) {
2167
+ for (let mode of ['select', 'delAll']) {
2168
+ let L = `${kind} ${mode}`
2169
+ //最長路徑邊界(三種鍵序)
2170
+ for (let order of ['q-first', 'p-first', 'tri']) {
2171
+ for (let [m, ok] of [[61, true], [62, true], [63, false], [64, false], [200, false]]) {
2172
+ stub.reset()
2173
+ if (ok) {
2174
+ await quiet(() => proc('u1', 'tests', mode, dagRoot(m, order)))
2175
+ assert.strictEqual(calls.length, 1, `${L} dag m=${m} ${order} 通過(最長路徑${m + 2})`)
2176
+ }
2177
+ else {
2178
+ assert.strictEqual(await rj(proc, mode, dagRoot(m, order)), deep(mode), `${L} dag m=${m} ${order} (最長路徑${m + 2})`)
2179
+ assert.strictEqual(calls.length, 0, `${L} dag m=${m} ORM不應被呼叫`)
2180
+ }
2181
+ done++
2182
+ }
2183
+ }
2184
+ //展開後物件數邊界(陣列不計): 根(1)+N個共用之葉各算1
2185
+ let leaf = { id: 'a1' }
2186
+ for (let [n, ok] of [[MAX_NODES - 1, true], [MAX_NODES, false]]) {
2187
+ stub.reset()
2188
+ let find = { $or: Array.from({ length: n }, () => leaf) }
2189
+ if (ok) {
2190
+ await quiet(() => proc('u1', 'tests', mode, find))
2191
+ assert.strictEqual(calls.length, 1, `${L} 兄弟${n}個(展開${n + 1})通過`)
2192
+ }
2193
+ else {
2194
+ assert.strictEqual(await rj(proc, mode, find), large(mode), `${L} 兄弟${n}個(展開${n + 1})`)
2195
+ assert.strictEqual(calls.length, 0, `${L} 兄弟${n}個 ORM不應被呼叫`)
2196
+ }
2197
+ done++
2198
+ }
2199
+ //無共用之樹(各葉為相異物件): 根(1)+N個葉(陣列不計); 恰100,000通過、100,001 reject(不能以「有無共用參考」捷徑略過展開數)
2200
+ for (let [n, ok] of [[MAX_NODES - 1, true], [MAX_NODES, false]]) {
2201
+ stub.reset()
2202
+ let tree = { $or: Array.from({ length: n }, () => ({ id: 'a1' })) }
2203
+ if (ok) {
2204
+ await quiet(() => proc('u1', 'tests', mode, tree))
2205
+ assert.strictEqual(calls.length, 1, `${L} 無共用之樹${n}葉(展開${n + 1})通過`)
2206
+ }
2207
+ else {
2208
+ assert.strictEqual(await rj(proc, mode, tree), large(mode), `${L} 無共用之樹${n}葉(展開${n + 1})`)
2209
+ assert.strictEqual(calls.length, 0, `${L} 無共用之樹${n}葉 ORM不應被呼叫`)
2210
+ }
2211
+ done++
2212
+ }
2213
+ //以Date為葉之深度邊界: Date不是容器, 恰64個容器(深端為{d: Date})通過、65個reject
2214
+ for (let [m, ok] of [[64, true], [65, false]]) {
2215
+ stub.reset()
2216
+ let x = { d: new Date() }
2217
+ for (let i = 1; i < m; i++) {
2218
+ x = { a: x }
2219
+ }
2220
+ if (ok) {
2221
+ await quiet(() => proc('u1', 'tests', mode, x))
2222
+ assert.strictEqual(calls.length, 1, `${L} Date葉 ${m}個容器通過`)
2223
+ }
2224
+ else {
2225
+ assert.strictEqual(await rj(proc, mode, x), deep(mode), `${L} Date葉 ${m}個容器`)
2226
+ assert.strictEqual(calls.length, 0, `${L} Date葉 ${m}個容器 ORM不應被呼叫`)
2227
+ }
2228
+ done++
2229
+ }
2230
+ //同上, 但共用之葉直接掛在根物件之N個鍵下(根自己累計): 根(1)+N個共用之葉; 恰100,000通過、100,001 reject(以根為累計者計入之邊界不同於陣列)
2231
+ for (let [n, ok] of [[MAX_NODES - 1, true], [MAX_NODES, false]]) {
2232
+ stub.reset()
2233
+ let wide = {}
2234
+ for (let i = 0; i < n; i++) {
2235
+ wide[`k${i}`] = leaf
2236
+ }
2237
+ if (ok) {
2238
+ await quiet(() => proc('u1', 'tests', mode, wide))
2239
+ assert.strictEqual(calls.length, 1, `${L} 根${n}鍵(展開${n + 1})通過`)
2240
+ }
2241
+ else {
2242
+ assert.strictEqual(await rj(proc, mode, wide), large(mode), `${L} 根${n}鍵(展開${n + 1})`)
2243
+ assert.strictEqual(calls.length, 0, `${L} 根${n}鍵 ORM不應被呼叫`)
2244
+ }
2245
+ done++
2246
+ }
2247
+ //指數型DAG: k=15 展開65,535個物件通過; k=16 展開131,071個物件 reject
2248
+ for (let [k, ok] of [[15, true], [16, false]]) {
2249
+ stub.reset()
2250
+ if (ok) {
2251
+ await quiet(() => proc('u1', 'tests', mode, expDag(k)))
2252
+ assert.strictEqual(calls.length, 1, `${L} expDag k=${k} 通過`)
2253
+ }
2254
+ else {
2255
+ assert.strictEqual(await rj(proc, mode, expDag(k)), large(mode), `${L} expDag k=${k}`)
2256
+ assert.strictEqual(calls.length, 0, `${L} expDag k=${k} ORM不應被呼叫`)
2257
+ }
2258
+ done++
2259
+ }
2260
+ //極端指數(含最長路徑僅63): 整數型/無該表者原物件交ORM不展開, 修前不致崩潰; 字串型/未知型於修前展開 2^31 個物件, 故只於不經castIdFind之表型態測(其餘由k=16~20涵蓋同一程式路徑)
2261
+ if (kind === 'int' || kind === 'none') {
2262
+ for (let [k, msg] of [[21, large(mode)], [31, large(mode)], [32, deep(mode)], [60, deep(mode)], [5000, deep(mode)]]) {
2263
+ stub.reset()
2264
+ let [err, ms] = await (async () => {
2265
+ let t = Date.now()
2266
+ let e = await rj(proc, mode, expDag(k))
2267
+ return [e, Date.now() - t]
2268
+ })()
2269
+ assert.strictEqual(err, msg, `${L} expDag k=${k}`)
2270
+ assert.ok(ms < 1000, `${L} expDag k=${k} 耗時 ${ms} ms`)
2271
+ assert.strictEqual(calls.length, 0, `${L} expDag k=${k} ORM不應被呼叫`)
2272
+ done++
2273
+ }
2274
+ }
2275
+ else {
2276
+ //k=20於修前約1秒、+213MB後交ORM; 修後立即reject
2277
+ stub.reset()
2278
+ let t = Date.now()
2279
+ assert.strictEqual(await rj(proc, mode, expDag(20)), large(mode), `${L} expDag k=20`)
2280
+ assert.ok(Date.now() - t < 1000, `${L} expDag k=20 耗時 ${Date.now() - t} ms`)
2281
+ assert.strictEqual(calls.length, 0, `${L} expDag k=20 ORM不應被呼叫`)
2282
+ done++
2283
+ }
2284
+ //循環(物件自指、$and自指、間接): 所有表型態皆固定訊息reject, ORM未被呼叫
2285
+ let cyc1 = { $and: [] }
2286
+ cyc1.$and.push(cyc1)
2287
+ let cyc2 = { id: 'a1' }
2288
+ cyc2.self = cyc2
2289
+ let cyc3 = { x: { y: { z: {} } } }
2290
+ cyc3.x.y.z.back = cyc3.x
2291
+ for (let [lab, cyc] of [['$and自指', cyc1], ['物件自指', cyc2], ['間接', cyc3]]) {
2292
+ stub.reset()
2293
+ assert.strictEqual(await rj(proc, mode, cyc), deep(mode), `${L} 循環 ${lab}`)
2294
+ assert.strictEqual(calls.length, 0, `${L} 循環 ${lab} ORM不應被呼叫`)
2295
+ done++
2296
+ }
2297
+ //合理之重用: 同一子條件於1000處(展開4,002個容器)照常
2298
+ let sub = { name: 'x' }
2299
+ stub.reset()
2300
+ await quiet(() => proc('u1', 'tests', mode, { $or: Array.from({ length: 1000 }, () => ({ $and: [sub, sub] })) }))
2301
+ assert.strictEqual(calls.length, 1, `${L} 合理重用`)
2302
+ done++
2303
+ }
2304
+ }
2305
+ //kinds 4(str、unknown、int、none) x mode 2 x [dag 10 + 兄弟2 + expDag 2 + 極端(int、none 5; str、unknown 1) + 循環3 + 重用1]
2306
+ assert.strictEqual(done, 2 * (2 * (15 + 2 + 2 + 2 + 2 + 2 + 5 + 3 + 1) + 2 * (15 + 2 + 2 + 2 + 2 + 2 + 1 + 3 + 1)), '完成數')
2307
+ })
2308
+
2309
+ it('S07 隨機分層DAG(固定種子, 樸素遞迴之BigInt預言機): 最長路徑>64或展開>100,000 ⇔ reject(too deep或too large), 兩側皆有案例', async function() {
2310
+ //規則5續; 預言機: 逐路徑展開之容器數與最長路徑, 獨立於被測之記憶化演算法
2311
+ let seed = 20261020
2312
+ let rnd = () => {
2313
+ seed = (seed * 1664525 + 1013904223) % 4294967296
2314
+ return seed / 4294967296
2315
+ }
2316
+ let ri = (a, b) => a + Math.floor(rnd() * (b - a + 1))
2317
+ let gen = () => {
2318
+ let L = ri(1, 40)
2319
+ let w = ri(1, 3)
2320
+ let f = ri(1, 3)
2321
+ let layers = []
2322
+ layers[L] = Array.from({ length: w }, () => ({ id: 'a' }))
2323
+ for (let l = L - 1; l >= 0; l--) {
2324
+ layers[l] = Array.from({ length: w }, () => ({ $and: Array.from({ length: ri(1, f) }, () => layers[l + 1][ri(0, w - 1)]) }))
2325
+ }
2326
+ return rnd() < 0.5 ? { $or: layers[0] } : layers[0][0] //前者令第0層全部置於淺層, 重現首次到達深度之繞過
2327
+ }
2328
+ let oracle = (root) => {
2329
+ let m = new Map()
2330
+ let go = (o) => {
2331
+ if (o === null || typeof o !== 'object') {
2332
+ return { d: 0, n: 0n }
2333
+ }
2334
+ if (m.has(o)) {
2335
+ return m.get(o)
2336
+ }
2337
+ let d = 0
2338
+ let n = Array.isArray(o) ? 0n : 1n //展開數只計物件(不含陣列)
2339
+ for (let k of Object.keys(o)) {
2340
+ let c = go(o[k])
2341
+ d = Math.max(d, c.d)
2342
+ n += c.n
2343
+ }
2344
+ let r = { d: d + 1, n }
2345
+ m.set(o, r)
2346
+ return r
2347
+ }
2348
+ return go(root)
2349
+ }
2350
+ let kinds = getKinds()
2351
+ let procInt = kinds.find((v) => v.kind === 'int').proc
2352
+ let procStr = kinds.find((v) => v.kind === 'str').proc
2353
+ let tally = { pass: 0, deep: 0, large: 0 }
2354
+ let N = 1500
2355
+ for (let i = 0; i < N; i++) {
2356
+ let root = gen()
2357
+ let o = oracle(root)
2358
+ let expectReject = o.d > MAX_DEPTH || o.n > BigInt(MAX_NODES)
2359
+ stub.reset()
2360
+ //整數型表: 全部案例(不經castIdFind, 修前亦不致展開)
2361
+ let r = await quiet(() => procInt('u1', 'tests', 'select', root).then(() => 'ok', (e) => String(e)))
2362
+ if (!expectReject) {
2363
+ assert.strictEqual(r, 'ok', `#${i} 預言機: 最長路徑${o.d} 展開${o.n} 應通過`)
2364
+ assert.strictEqual(calls.length, 1, `#${i}`)
2365
+ tally.pass++
2366
+ //字串型表: 僅預言機判通過者(展開 ≤ 100,000, 修前亦安全)
2367
+ stub.reset()
2368
+ await quiet(() => procStr('u1', 'tests', 'select', root))
2369
+ assert.strictEqual(calls.length, 1, `#${i} 字串型表通過`)
2370
+ }
2371
+ else {
2372
+ assert.ok(r === deep('select') || r === large('select'), `#${i} 預言機: 最長路徑${o.d} 展開${o.n} 應reject, 實得 ${r}`)
2373
+ assert.strictEqual(calls.length, 0, `#${i} ORM不應被呼叫`)
2374
+ if (r === deep('select')) {
2375
+ tally.deep++
2376
+ }
2377
+ else {
2378
+ tally.large++
2379
+ }
2380
+ }
2381
+ }
2382
+ assert.strictEqual(tally.pass + tally.deep + tally.large, N, '完成數')
2383
+ assert.ok(tally.pass > 300 && tally.deep > 100 && tally.large > 100, `三類皆有足量案例 ${JSON.stringify(tally)}`)
2384
+ })
2385
+
2386
+ })
2387
+
2388
+
2389
+ describe('JSON來源之一般輸入行為鎖定(S08): 第82項修正不改變資料通道可達之輸入', function() {
2390
+
2391
+ beforeEach(function() {
2392
+ stub.reset()
2393
+ })
2394
+
2395
+ //預言機: 樸素之參考實作(R-ID-02 轉型、R-ID-01 補值、R-INPUT-01 形狀), 輸入一律為 JSON.parse(JSON.stringify()) 之樹, 與修正前後之行為皆相同
2396
+ //(同一語料於 HEAD 之 src 亦全綠, 證明預言機本身正確; 修正後仍全綠, 證明 JSON 來源之行為未變)
2397
+ const GEN = '<GEN>'
2398
+ const OPS = ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin']
2399
+ let castId = (v, cast) => (cast && typeof v === 'number' && Number.isFinite(v) ? String(v) : v)
2400
+ let refCond = (c, cast) => {
2401
+ if (!cast) {
2402
+ return c
2403
+ }
2404
+ if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
2405
+ let r = {}
2406
+ for (let k of Object.keys(c)) {
2407
+ r[k] = OPS.includes(k) ? (Array.isArray(c[k]) ? c[k].map((x) => castId(x, true)) : castId(c[k], true)) : c[k]
2408
+ }
2409
+ return r
2410
+ }
2411
+ return castId(c, true)
2412
+ }
2413
+ let refFind = (f, cast) => {
2414
+ if (!cast || f === null || typeof f !== 'object') {
2415
+ return f
2416
+ }
2417
+ let r = {}
2418
+ for (let k of Object.keys(f)) {
2419
+ if (k === 'id') {
2420
+ r[k] = refCond(f[k], true)
2421
+ }
2422
+ else if ((k === '$and' || k === '$or') && Array.isArray(f[k])) {
2423
+ r[k] = f[k].map((x) => refFind(x, true))
2424
+ }
2425
+ else {
2426
+ r[k] = f[k]
2427
+ }
2428
+ }
2429
+ return r
2430
+ }
2431
+ //寫入列: 轉型後, 有效輸入(單一物件須非空; 陣列須長度>=2, 或長度1而該列非空)才補主鍵(R-ID-01 規則3)
2432
+ let refWrite = (data, cast) => {
2433
+ let one = !Array.isArray(data)
2434
+ let rows = (one ? [data] : data).map((v) => ({ ...v, ...(cast && 'id' in v ? { id: castId(v.id, true) } : {}) }))
2435
+ let effective = one ? Object.keys(rows[0]).length > 0 : (rows.length >= 2 || (rows.length === 1 && Object.keys(rows[0]).length > 0))
2436
+ if (cast && effective) {
2437
+ rows = rows.map((v) => (typeof v.id === 'string' && v.id.length > 0 ? v : { ...v, id: GEN }))
2438
+ }
2439
+ return one ? rows[0] : rows
2440
+ }
2441
+ let refDel = (data, cast) => {
2442
+ let one = !Array.isArray(data)
2443
+ let rows = (one ? [data] : data).map((v) => (cast && typeof v.id === 'number' && Number.isFinite(v.id) ? { ...v, id: String(v.id) } : v))
2444
+ return one ? rows[0] : rows
2445
+ }
2446
+ let norm = (d) => {
2447
+ let nr = (r) => (r !== null && typeof r === 'object' && isGen(r.id) ? { ...r, id: GEN } : r)
2448
+ return Array.isArray(d) ? d.map(nr) : nr(d)
2449
+ }
2450
+
2451
+ //語料(固定種子)
2452
+ let seed = 8008
2453
+ let rnd = () => {
2454
+ seed = (seed * 1664525 + 1013904223) % 4294967296
2455
+ return seed / 4294967296
2456
+ }
2457
+ let pick = (a) => a[Math.floor(rnd() * a.length)]
2458
+ let STR = ['', 'a1', 'a2', 'beta', '中文', 'x y', '12', 'length', 'true']
2459
+ let NUM = [0, 1, 5, 123, -1, 1.5, 1e21, 0.1]
2460
+ let KEY = ['name', 'meta', 'length', 'x', 'y', 'arr']
2461
+ let scalar = () => {
2462
+ let r = rnd()
2463
+ return r < 0.45 ? pick(STR) : r < 0.8 ? pick(NUM) : r < 0.9 ? null : rnd() < 0.5
2464
+ }
2465
+ let value = (d) => {
2466
+ let r = rnd()
2467
+ if (d <= 0 || r < 0.5) {
2468
+ return scalar()
2469
+ }
2470
+ if (r < 0.75) {
2471
+ return Array.from({ length: Math.floor(rnd() * 4) }, () => value(d - 1))
2472
+ }
2473
+ let o = {}
2474
+ for (let i = Math.floor(rnd() * 4); i > 0; i--) {
2475
+ o[pick(KEY)] = value(d - 1)
2476
+ }
2477
+ return o
2478
+ }
2479
+ //row(strPool, numBase): id 之形態: 非空字串、有限數字、null、''、布林、物件、陣列、缺
2480
+ //special: 非mix之入口另含特殊數字id(0、負數、小數、1e21)——mix各段之id須互不相同, 不含
2481
+ let row = (sp, nb, special = false) => {
2482
+ let o = {}
2483
+ let r = rnd()
2484
+ if (r < 0.4) {
2485
+ o.id = `${sp}${Math.floor(rnd() * 5)}`
2486
+ }
2487
+ else if (r < 0.65) {
2488
+ o.id = special && rnd() < 0.5 ? pick([0, -1, 1.5, 1e21, 0.1]) : nb + Math.floor(rnd() * 5)
2489
+ }
2490
+ else if (r < 0.7) {
2491
+ o.id = null
2492
+ }
2493
+ else if (r < 0.75) {
2494
+ o.id = ''
2495
+ }
2496
+ else if (r < 0.8) {
2497
+ o.id = rnd() < 0.5
2498
+ }
2499
+ else if (r < 0.85) {
2500
+ o.id = rnd() < 0.5 ? { a: 1 } : [1]
2501
+ }
2502
+ for (let i = Math.floor(rnd() * 4); i > 0; i--) {
2503
+ o[pick(KEY)] = value(3)
2504
+ }
2505
+ return o
2506
+ }
2507
+ let rows = (sp, nb, special = false) => {
2508
+ let r = rnd()
2509
+ return r < 0.25 ? row(sp, nb, special) : r < 0.3 ? [] : r < 0.35 ? {} : r < 0.4 ? [{}] : Array.from({ length: 1 + Math.floor(rnd() * 4) }, () => row(sp, nb, special))
2510
+ }
2511
+ let idCond = () => {
2512
+ let r = rnd()
2513
+ if (r < 0.4) {
2514
+ return scalar()
2515
+ }
2516
+ let o = {}
2517
+ for (let i = 1 + Math.floor(rnd() * 2); i > 0; i--) {
2518
+ let op = pick([...OPS, '$regex'])
2519
+ o[op] = op === '$in' || op === '$nin' ? Array.from({ length: Math.floor(rnd() * 4) }, scalar) : scalar()
2520
+ }
2521
+ return o
2522
+ }
2523
+ let find = (d) => {
2524
+ let o = {}
2525
+ if (rnd() < 0.6) {
2526
+ o.id = idCond()
2527
+ }
2528
+ if (rnd() < 0.5) {
2529
+ o.name = scalar()
2530
+ }
2531
+ if (rnd() < 0.2) {
2532
+ o.length = pick([0, 1, 3.5, '2'])
2533
+ }
2534
+ if (d > 0 && rnd() < 0.35) {
2535
+ o[pick(['$and', '$or'])] = Array.from({ length: Math.floor(rnd() * 3) }, () => find(d - 1))
2536
+ }
2537
+ return o
2538
+ }
2539
+ let topFind = () => {
2540
+ let r = rnd()
2541
+ return r < 0.04 ? null : r < 0.07 ? [] : r < 0.09 ? 'x' : r < 0.11 ? 5 : r < 0.15 ? undefined : find(3)
2542
+ }
2543
+ let json = (v) => (v === undefined ? undefined : JSON.parse(JSON.stringify(v)))
2544
+ let N = 400
2545
+
2546
+ it('S08 JSON來源之列與條件(insert、save、del、mix、select、delAll x 字串型/未知型/整數型/無該表): ORM收到之實參與參考實作逐組相同', async function() {
2547
+ //R-ID-02、R-ID-01、R-INPUT-01 於 JSON 可表達之輸入上的行為; 第82項修正(規則7、規則5續)不得改變
2548
+ let done = 0
2549
+ let tally = { ok: 0, reject: 0 }
2550
+ for (let { kind, proc } of getKinds()) {
2551
+ let cast = kind === 'str' || kind === 'unknown'
2552
+ let modes = kind === 'none' ? ['del', 'select', 'delAll'] : ['insert', 'save', 'del', 'mix', 'select', 'delAll']
2553
+ for (let mode of modes) {
2554
+ for (let i = 0; i < N; i++) {
2555
+ stub.reset()
2556
+ let L = `${kind} ${mode} #${i}`
2557
+ let input
2558
+ let expectErr = null
2559
+ let expectCalls
2560
+ if (mode === 'insert' || mode === 'save') {
2561
+ input = json(rows('A', 1000, true))
2562
+ expectCalls = [{ fn: mode, data: refWrite(json(input), cast) }]
2563
+ }
2564
+ else if (mode === 'del') {
2565
+ input = json(rows('D', 2000, true))
2566
+ expectCalls = [{ fn: 'del', data: refDel(json(input), cast) }]
2567
+ }
2568
+ else if (mode === 'mix') {
2569
+ input = {}
2570
+ let add = rnd() < 0.7 ? json(Array.from({ length: Math.floor(rnd() * 3) }, () => row('A', 1000))) : undefined
2571
+ let del = rnd() < 0.7 ? json(Array.from({ length: Math.floor(rnd() * 3) }, () => row('D', 2000))) : undefined
2572
+ let diff = rnd() < 0.5 ? json(Array.from({ length: Math.floor(rnd() * 3) }, () => row('F', 3000))) : undefined
2573
+ for (let [k, v] of [['add', add], ['del', del], ['diff', diff]]) {
2574
+ if (v !== undefined) {
2575
+ input[k] = v
2576
+ }
2577
+ }
2578
+ if (Object.keys(input).length === 0) {
2579
+ input.add = []
2580
+ }
2581
+ expectCalls = []
2582
+ if (add && add.length > 0) {
2583
+ expectCalls.push({ fn: 'insert', data: refWrite(json(add), cast) })
2584
+ }
2585
+ if (del && del.length > 0) {
2586
+ expectCalls.push({ fn: 'del', data: refDel(json(del), cast) })
2587
+ }
2588
+ if (diff && diff.length > 0) {
2589
+ expectCalls.push({ fn: 'save', data: refWrite(json(diff), cast) })
2590
+ }
2591
+ }
2592
+ else {
2593
+ input = topFind()
2594
+ if (input !== undefined && (input === null || typeof input !== 'object' || Array.isArray(input))) {
2595
+ expectErr = `invalid find of ${mode}, need an object`
2596
+ expectCalls = []
2597
+ }
2598
+ else {
2599
+ input = json(input)
2600
+ expectCalls = [{ fn: mode, data: refFind(json(input), cast) }]
2601
+ }
2602
+ }
2603
+ let got = await quiet(() => proc('u1', 'tests', mode, json(input)).then(() => null, (e) => String(e)))
2604
+ assert.strictEqual(got, expectErr, `${L} reject`)
2605
+ same(calls.map((v) => ({ fn: v.fn, data: norm(v.data) })), expectCalls, `${L} ORM收到`)
2606
+ tally[expectErr === null ? 'ok' : 'reject']++
2607
+ done++
2608
+ }
2609
+ }
2610
+ }
2611
+ assert.strictEqual(done, N * (3 * 6 + 3), '完成數')
2612
+ assert.ok(tally.ok > 7000 && tally.reject > 100, `通過與reject兩側皆有案例 ${JSON.stringify(tally)}`)
2613
+ })
2614
+
2615
+ })
2616
+
2617
+
2618
+ //R-OBJ-01 規則7 續(第82項 run02, 外審 sol 第一輪 F01~F04、F06~F09): 通過檢查之後不再執行使用端程式, 且檢查之值即使用之值
2619
+ describe('R-OBJ-01 規則7 續 檢查之後不執行使用端程式(S09~S14)', function() {
2620
+
2621
+ beforeEach(function() {
2622
+ stub.reset()
2623
+ })
2624
+
2625
+ let KINDS = ['str', 'unknown', 'int']
2626
+ let IRREG = 'unsupported irregular object or array'
2627
+ let msgWrite = { 'insert': 'failed to insert data', 'save': 'failed to save data', 'mix.add': 'failed to insert data', 'mix.diff': 'failed to update data' }
2628
+ let runW = (proc, entry, row) => {
2629
+ switch (entry) {
2630
+ case 'insert': return proc('u1', 'tests', 'insert', [row])
2631
+ case 'save': return proc('u1', 'tests', 'save', [row])
2632
+ case 'mix.add': return proc('u1', 'tests', 'mix', { add: [row] })
2633
+ case 'mix.diff': return proc('u1', 'tests', 'mix', { diff: [row] })
2634
+ }
2635
+ throw new Error(`entry ${entry}`)
2636
+ }
2637
+ let rjS = (proc, mode, input) => quiet(() => getReject(proc('u1', 'tests', mode, input)))
2638
+ let hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k)
2639
+ let firstRow = () => (Array.isArray(calls[0].data) ? calls[0].data[0] : calls[0].data)
2640
+ let okRow = () => ({ id: 'a1', name: 'x' })
2641
+
2642
+ it('S09 函數型列(宣稱Object標籤之函數、callable Proxy、標籤getter拋使用端字串者): 寫入列與mix.del列整批irregular, 陷阱與getter 0次, 不轉出使用端字串; 無標籤之普通函數維持not an object(F02)', async function() {
2643
+ //規則7 ②; 修前: isIrregularRow 以 typeof object 為前提而略過函數, isobj 卻相信函數之標籤而當成列複製; 標籤getter拋字串時被邊界保證當成本層固定訊息原樣轉出
2644
+ let reads = 0
2645
+ let tagged = () => {
2646
+ let f = function() {}
2647
+ f.id = 'f1'
2648
+ f[Symbol.toStringTag] = 'Object'
2649
+ return f
2650
+ }
2651
+ let callableProxy = () => new Proxy(function() {}, {
2652
+ get(t, k, r) {
2653
+ reads++
2654
+ return Reflect.get(t, k, r)
2655
+ },
2656
+ getOwnPropertyDescriptor(t, k) {
2657
+ reads++
2658
+ return Reflect.getOwnPropertyDescriptor(t, k)
2659
+ },
2660
+ })
2661
+ let throwing = () => {
2662
+ let f = function() {}
2663
+ Object.defineProperty(f, Symbol.toStringTag, {
2664
+ get() {
2665
+ reads++
2666
+ throw 'SECRET-USER-STRING' // eslint-disable-line no-throw-literal
2667
+ },
2668
+ })
2669
+ return f
2670
+ }
2671
+ let done = 0
2672
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
2673
+ for (let [lab, mk] of [['宣稱Object標籤之函數', tagged], ['callable Proxy', callableProxy], ['標籤getter拋使用端字串之函數', throwing]]) {
2674
+ for (let entry of ['insert', 'save', 'mix.add', 'mix.diff']) {
2675
+ stub.reset()
2676
+ let err = await quiet(() => getReject(runW(proc, entry, mk())))
2677
+ assert.strictEqual(err, `${msgWrite[entry]}, ${IRREG}: data[0]`, `${kind} ${lab} ${entry}`)
2678
+ assert.deepStrictEqual(calls, [], `${kind} ${lab} ${entry} ORM不應被呼叫`)
2679
+ done++
2680
+ }
2681
+ stub.reset()
2682
+ let err = await quiet(() => getReject(proc('u1', 'tests', 'mix', { add: [okRow()], del: [mk()] })))
2683
+ assert.strictEqual(err, `failed to delete data, ${IRREG}: data[0]`, `${kind} ${lab} mix.del`)
2684
+ assert.deepStrictEqual(calls, [], `${kind} ${lab} mix.del ORM不應被呼叫`)
2685
+ done++
2686
+ }
2687
+ assert.strictEqual(reads, 0, `${kind} 陷阱與getter不被執行`)
2688
+ //對照: 無標籤之普通函數維持既有之 not an object; mix.del 之普通函數同單獨del交後端逐筆處置
2689
+ stub.reset()
2690
+ assert.strictEqual(await rjS(proc, 'insert', [function() {}]), 'failed to insert data, not an object: data[0]', `${kind} 普通函數`)
2691
+ stub.reset()
2692
+ await quiet(() => proc('u1', 'tests', 'mix', { del: [function() {}] }))
2693
+ assert.strictEqual(calls.length, 1, `${kind} mix.del 普通函數交後端`)
2694
+ }
2695
+ assert.strictEqual(done, 3 * 3 * 5, '完成數')
2696
+ })
2697
+
2698
+ it('S10 通過檢查後之複製不執行getter: 數字id列帶可列舉Symbol getter、del列帶其他getter, 皆0次; 模擬後端於del步驟讀取del列時其副作用不得改動尚未寫入之add與diff(F03)', async function() {
2699
+ //規則7 ①; 修前: castIdRows 以 { ...v, id } 展開, 會讀取可列舉Symbol鍵之getter與del列之其他getter(此時寫入列已通過鍵檢查而尚未clearXSS), getter可向已檢查之meta加入自有__proto__
2700
+ let reads = 0
2701
+ let mkSym = () => {
2702
+ let row = { id: 123, name: 'n', meta: {} }
2703
+ Object.defineProperty(row, Symbol('s'), {
2704
+ enumerable: true,
2705
+ get() {
2706
+ reads++
2707
+ Object.defineProperty(row.meta, '__proto__', { value: { unchecked: 1 }, enumerable: true, configurable: true, writable: true })
2708
+ return 0
2709
+ },
2710
+ })
2711
+ return row
2712
+ }
2713
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
2714
+ for (let entry of ['insert', 'save', 'mix.add', 'mix.diff']) {
2715
+ stub.reset()
2716
+ await quiet(() => runW(proc, entry, mkSym()))
2717
+ assert.strictEqual(calls.length, 1, `${kind} ${entry}`)
2718
+ assert.strictEqual(reads, 0, `${kind} ${entry} Symbol getter不被執行`)
2719
+ assert.ok(!hasOwn(firstRow().meta, '__proto__'), `${kind} ${entry} 寫入之meta無自有__proto__`)
2720
+ }
2721
+ }
2722
+ //模擬後端: del步驟讀取del列之全部自有鍵(真ORM之cloneDeep會執行getter)
2723
+ let calls2 = []
2724
+ let WOrmReading = function() {
2725
+ let ev = {}
2726
+ for (let fn of ['select', 'insert', 'save', 'del', 'delAll']) {
2727
+ ev[fn] = async (data) => {
2728
+ calls2.push({ fn, data })
2729
+ if (fn === 'del') {
2730
+ for (let row of data) {
2731
+ for (let k of Object.keys(row)) {
2732
+ row[k] //eslint-disable-line no-unused-expressions
2733
+ }
2734
+ }
2735
+ }
2736
+ let n = Array.isArray(data) ? data.length : 1
2737
+ return fn === 'insert' || fn === 'delAll' ? { n: 1, nInserted: 1, ok: 1 } : Array.from({ length: n }, () => ({ n: 1, nModified: 1, nDeleted: 1, ok: 1 }))
2738
+ }
2739
+ }
2740
+ return ev
2741
+ }
2742
+ for (let [kind, ds] of [['str', dsStr], ['int', dsInt]]) {
2743
+ let procR = WServOrm(ds, WOrmReading, 'u', 'd').procOrm
2744
+ calls2.length = 0
2745
+ reads = 0
2746
+ let addRow = { id: 'n1', name: 'add', meta: {} }
2747
+ let diffRow = { id: 'f1', name: 'diff', meta: {} }
2748
+ let delRow = {
2749
+ id: 5,
2750
+ get unused() {
2751
+ reads++
2752
+ for (let r of [addRow, diffRow]) {
2753
+ Object.defineProperty(r.meta, '__proto__', { value: { unchecked: 1 }, enumerable: true, configurable: true, writable: true })
2754
+ }
2755
+ return 0
2756
+ },
2757
+ }
2758
+ await quiet(() => procR('u1', 'tests', 'mix', { add: [addRow], del: [delRow], diff: [diffRow] }))
2759
+ assert.deepStrictEqual(calls2.map((v) => v.fn), ['insert', 'del', 'save'], `${kind} 呼叫序`)
2760
+ assert.strictEqual(reads, 1, `${kind} getter僅由後端於del步驟執行一次(本層不執行)`)
2761
+ for (let c of calls2.filter((v) => v.fn !== 'del')) {
2762
+ assert.ok(!hasOwn(c.data[0].meta, '__proto__'), `${kind} ${c.fn} 寫入之meta無自有__proto__(adjustData之深複本早於del步驟)`)
2763
+ }
2764
+ }
2765
+ })
2766
+
2767
+ it('S11 稽核欄位(userId、timeCreate、userIdUpdate、timeUpdate)以defineProperty寫入, 不執行原型上之setter: 不改寫id、不加入未檢查之欄位; 凍結列仍為failed to adjust data; 字串主鍵列之原物件仍被寫入稽核欄位(既有行為鎖定)(F04)', async function() {
2768
+ //規則7; 修前: adjustData 以賦值寫入稽核欄位, 類別DTO之原型setter在跨段同鍵檢查之後被執行, 可改寫id(使檢查所見之id不同於實際寫入者)並向列加入含自有__proto__之欄位
2769
+ let keys = ['id', 'name', 'meta', 'userId', 'timeCreate', 'userIdUpdate', 'timeUpdate']
2770
+ let dsA = {
2771
+ str: { tests: { keys, settings: { id: { pk: true, type: 'STRING' }, name: { type: 'STRING' } } } },
2772
+ unknown: { tests: { keys } },
2773
+ int: { tests: { keys, settings: { id: { pk: true, type: 'INTEGER' }, name: { type: 'STRING' } } } },
2774
+ }
2775
+ let reads = 0
2776
+ class Dto {
2777
+ constructor(id) {
2778
+ this.id = id
2779
+ this.name = 'n'
2780
+ this.meta = {}
2781
+ }
2782
+
2783
+ set userIdUpdate(v) { // eslint-disable-line accessor-pairs
2784
+ reads++
2785
+ this.id = 'keep'
2786
+ Object.defineProperty(this.meta, '__proto__', { value: { unchecked: 1 }, enumerable: true, configurable: true, writable: true })
2787
+ }
2788
+ }
2789
+ for (let kind of KINDS) {
2790
+ let proc = WServOrm(dsA[kind], WOrmStub, 'u', 'd').procOrm
2791
+ for (let entry of ['insert', 'save', 'mix.add', 'mix.diff']) {
2792
+ stub.reset()
2793
+ let row = new Dto('new')
2794
+ await quiet(() => runW(proc, entry, row))
2795
+ assert.strictEqual(calls.length, 1, `${kind} ${entry}`)
2796
+ assert.strictEqual(reads, 0, `${kind} ${entry} 原型setter不被執行`)
2797
+ let got = firstRow()
2798
+ assert.strictEqual(got.id, 'new', `${kind} ${entry} id不被改寫`)
2799
+ assert.ok(!hasOwn(got.meta, '__proto__'), `${kind} ${entry} 無未檢查之欄位`)
2800
+ assert.strictEqual(got.userIdUpdate, 'u1', `${kind} ${entry} 稽核欄位已寫入`)
2801
+ assert.ok(typeof got.timeUpdate === 'string' && got.timeUpdate.length > 0, `${kind} ${entry} timeUpdate`)
2802
+ }
2803
+ //凍結之列: 寫入稽核欄位拋TypeError, 轉為固定字串
2804
+ stub.reset()
2805
+ assert.strictEqual(await rjS(proc, 'insert', [Object.freeze({ id: 'a', name: 'n' })]), 'failed to adjust data', `${kind} 凍結列`)
2806
+ assert.deepStrictEqual(calls, [], `${kind} 凍結列 ORM不應被呼叫`)
2807
+ }
2808
+ //既有行為鎖定(帳本 R-OBJ-01 已知限制): 字串主鍵之列(未經轉型複製)其原物件被寫入稽核欄位
2809
+ let procS = WServOrm(dsA.str, WOrmStub, 'u', 'd').procOrm
2810
+ let own = { id: 'a1', name: 'n' }
2811
+ await quiet(() => procS('u1', 'tests', 'insert', [own]))
2812
+ assert.strictEqual(own.userId, 'u1', '原物件被寫入稽核欄位(既有行為)')
2813
+ })
2814
+
2815
+ it('S12 存取器之判準以描述子有無value欄位為準: get與set皆為undefined之存取器id/欄位仍視為存取器(寫入列與find整批reject, mix.del列整批reject)(F07)', async function() {
2816
+ //規則3、7; 修前: 以 d.get !== undefined || d.set !== undefined 判斷, 漏掉 { get: undefined, set: undefined }
2817
+ let emptyAcc = (o, k, enumerable = true) => Object.defineProperty(o, k, { get: undefined, set: undefined, enumerable, configurable: true })
2818
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
2819
+ for (let entry of ['insert', 'save', 'mix.add', 'mix.diff']) {
2820
+ stub.reset()
2821
+ assert.strictEqual(await quiet(() => getReject(runW(proc, entry, emptyAcc({ name: 'x' }, 'id')))), `${msgWrite[entry]}, unsupported accessor property: data[0]`, `${kind} ${entry} id`)
2822
+ assert.strictEqual(await quiet(() => getReject(runW(proc, entry, emptyAcc({ id: 'a1' }, 'name')))), `${msgWrite[entry]}, unsupported accessor property: data[0]`, `${kind} ${entry} 欄位`)
2823
+ assert.deepStrictEqual(calls, [], `${kind} ${entry} ORM不應被呼叫`)
2824
+ }
2825
+ for (let enumerable of [true, false]) {
2826
+ stub.reset()
2827
+ assert.strictEqual(await quiet(() => getReject(proc('u1', 'tests', 'mix', { add: [{ id: 'a1' }], del: [emptyAcc({}, 'id', enumerable)] }))), `failed to delete data, ${IRREG}: data[0]`, `${kind} mix.del enumerable=${enumerable}`)
2828
+ assert.deepStrictEqual(calls, [], `${kind} mix.del ORM不應被呼叫`)
2829
+ }
2830
+ for (let mode of ['select', 'delAll']) {
2831
+ assert.strictEqual(await rjS(proc, mode, emptyAcc({}, 'name')), `invalid find of ${mode}, unsupported accessor property`, `${kind} ${mode}`)
2832
+ }
2833
+ //不可列舉之空存取器id(寫入列): 與不可列舉之存取器同, 視為未提供
2834
+ stub.reset()
2835
+ await quiet(() => proc('u1', 'tests', 'insert', [emptyAcc({ name: 'x' }, 'id', false)]))
2836
+ assert.strictEqual(calls.length, 1, `${kind} 不可列舉之空存取器id視為未提供`)
2837
+ assert.ok(!hasOwn(firstRow(), 'id') || isGen(firstRow().id), `${kind} 補主鍵或無id`)
2838
+ }
2839
+ })
2840
+
2841
+ it('S13 原型鏈長度上限(MAX_CHAIN: 鏈上至多65個物件含自身): 共用陣列帶長原型鏈而被引用多次時分類成本有界; 恰在上限通過、逾1層以irregular拒收(F08)', async function() {
2842
+ //規則5續、6; 修前: measureFind 對每條邊重新分類而重掃原型鏈, E 條邊 x P 層原型為平方; 現鏈長逾上限者視為不乾淨
2843
+ let withChain = (arr, k) => {
2844
+ //arr -> p1 -> ... -> pk -> Array.prototype -> Object.prototype -> null; 鏈上物件數 = 1 + k + 2
2845
+ let top = Array.prototype
2846
+ for (let i = 0; i < k; i++) {
2847
+ top = Object.create(top)
2848
+ }
2849
+ return Object.setPrototypeOf(arr, top)
2850
+ }
2851
+ for (let { kind, proc } of getKinds()) {
2852
+ for (let mode of ['select', 'delAll']) {
2853
+ let L = `${kind} ${mode}`
2854
+ stub.reset()
2855
+ await quiet(() => proc('u1', 'tests', mode, { id: { $in: withChain([], 61) } })) //鏈上64個物件
2856
+ assert.strictEqual(calls.length, 1, `${L} 鏈上64個物件通過`)
2857
+ stub.reset()
2858
+ await quiet(() => proc('u1', 'tests', mode, { id: { $in: withChain([], 62) } })) //65個(上限)
2859
+ assert.strictEqual(calls.length, 1, `${L} 鏈上65個物件通過(上限)`)
2860
+ stub.reset()
2861
+ assert.strictEqual(await rjS(proc, mode, { id: { $in: withChain([], 63) } }), `invalid find of ${mode}, ${IRREG}`, `${L} 鏈上66個物件`)
2862
+ assert.strictEqual(calls.length, 0, `${L} ORM不應被呼叫`)
2863
+ //極長之鏈: 不逐層走完(成本有界)
2864
+ let t = Date.now()
2865
+ stub.reset()
2866
+ assert.strictEqual(await rjS(proc, mode, { id: { $in: withChain([], 200000) } }), `invalid find of ${mode}, ${IRREG}`, `${L} 20萬層`)
2867
+ assert.ok(Date.now() - t < 1000, `${L} 20萬層耗時 ${Date.now() - t} ms`)
2868
+ //共用之長鏈陣列被引用多次(E x P 有界)
2869
+ let shared = withChain([], 62)
2870
+ t = Date.now()
2871
+ stub.reset()
2872
+ await quiet(() => proc('u1', 'tests', mode, { $or: Array.from({ length: 20000 }, () => shared) }))
2873
+ assert.strictEqual(calls.length, 1, `${L} 共用長鏈陣列2萬次通過`)
2874
+ assert.ok(Date.now() - t < 3000, `${L} 共用長鏈陣列2萬次耗時 ${Date.now() - t} ms`)
2875
+ }
2876
+ }
2877
+ })
2878
+
2879
+ it('S15 mix之input本身: Proxy(含已撤銷者)、原型鏈有標籤者於讀取任何屬性之前整批reject, 陷阱0次; add、del、diff只取input之自有資料屬性(繼承者不取、不執行原型上之getter)(minimax F3)', async function() {
2880
+ //規則7、R-INPUT-01 規則2; 修前: iseobj(input)讀標籤、input.add讀屬性, 對Proxy執行陷阱8次, 對繼承之存取器執行原型上之getter
2881
+ let reads = 0
2882
+ let counted = (target) => new Proxy(target, {
2883
+ get(t, k, r) {
2884
+ reads++
2885
+ return Reflect.get(t, k, r)
2886
+ },
2887
+ has(t, k) {
2888
+ reads++
2889
+ return Reflect.has(t, k)
2890
+ },
2891
+ ownKeys(t) {
2892
+ reads++
2893
+ return Reflect.ownKeys(t)
2894
+ },
2895
+ getOwnPropertyDescriptor(t, k) {
2896
+ reads++
2897
+ return Reflect.getOwnPropertyDescriptor(t, k)
2898
+ },
2899
+ })
2900
+ let revoked = () => {
2901
+ let rv = Proxy.revocable({ add: [okRow()] }, {})
2902
+ rv.revoke()
2903
+ return rv.proxy
2904
+ }
2905
+ let NEED = 'invalid input of mix, need the result object of ltdtDiffByKey'
2906
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
2907
+ for (let [lab, mk] of [
2908
+ ['Proxy', () => counted({ add: [okRow()] })],
2909
+ ['已撤銷之Proxy', revoked],
2910
+ ['原型鏈有標籤', () => Object.assign(Object.create(Object.assign(Object.create(null), { [Symbol.toStringTag]: 'X' })), { add: [okRow()] })],
2911
+ ['自有標籤', () => ({ add: [okRow()], [Symbol.toStringTag]: 'X' })],
2912
+ ]) {
2913
+ stub.reset()
2914
+ assert.strictEqual(await rjS(proc, 'mix', mk()), NEED, `${kind} ${lab}`)
2915
+ assert.deepStrictEqual(calls, [], `${kind} ${lab} ORM不應被呼叫`)
2916
+ }
2917
+ assert.strictEqual(reads, 0, `${kind} 陷阱不被執行`)
2918
+ //繼承之存取器add: 不取(原型上之getter不被執行); 無自有add、del、diff者為無操作
2919
+ let inheritedReads = 0
2920
+ class MixIn {
2921
+ get add() {
2922
+ inheritedReads++
2923
+ return [okRow()]
2924
+ }
2925
+ }
2926
+ let mi = Object.assign(new MixIn(), { diff: [{ id: 'f1', name: 'd' }] })
2927
+ stub.reset()
2928
+ await quiet(() => proc('u1', 'tests', 'mix', mi))
2929
+ assert.strictEqual(inheritedReads, 0, `${kind} 原型上之getter不被執行`)
2930
+ assert.deepStrictEqual(calls.map((v) => v.fn), ['save'], `${kind} 只處理自有之diff`)
2931
+ //對照: 類別實例而add、del、diff為自有資料屬性者照常
2932
+ class Plain {
2933
+ constructor() {
2934
+ this.add = [{ id: 'n1', name: 'a' }]
2935
+ }
2936
+ }
2937
+ stub.reset()
2938
+ await quiet(() => proc('u1', 'tests', 'mix', new Plain()))
2939
+ assert.deepStrictEqual(calls.map((v) => v.fn), ['insert'], `${kind} 類別實例照常`)
2940
+ }
2941
+ })
2942
+
2943
+ it('S14 被拒之批次容器與列再被呼叫端重用: 診斷輸出(真格式化, 非quiet)不執行inspect.custom與標籤getter, 同一物件第二次呼叫仍同結果, 物件不被改動(F09)', async function() {
2944
+ //規則7、R-OBJ-01 規則6 ⑤ 延伸至寫入路徑; 修前若拒收路徑 console.log 原物件(util.inspect 讀標籤並呼叫 inspect.custom), 其副作用可在拒收後改動呼叫端物件
2945
+ let reads = 0
2946
+ let withProbe = (o) => {
2947
+ o[util.inspect.custom] = () => {
2948
+ reads++
2949
+ return 'x'
2950
+ }
2951
+ return o
2952
+ }
2953
+ let items = [
2954
+ ['原型鏈有標籤getter之陣列(容器)', 'container', () => withProbe(Object.setPrototypeOf([okRow()], Object.create(Array.prototype, {
2955
+ [Symbol.toStringTag]: {
2956
+ get() {
2957
+ reads++
2958
+ return 'Array'
2959
+ },
2960
+ },
2961
+ })))],
2962
+ ['有空洞之陣列(容器)', 'container', () => withProbe(Object.assign([okRow(), , okRow()], { extra: 1 }))], // eslint-disable-line no-sparse-arrays
2963
+ ['索引為存取器之陣列(容器)', 'container', () => withProbe(Object.defineProperty([okRow()], 0, {
2964
+ enumerable: true,
2965
+ get() {
2966
+ reads++
2967
+ return okRow()
2968
+ },
2969
+ }))],
2970
+ ['帶標籤與inspect之列', 'row', () => withProbe({ id: 'a1', [Symbol.toStringTag]: 'X' })],
2971
+ ]
2972
+ let logOrig = console.log
2973
+ console.log = (...a) => {
2974
+ util.format(...a) //同真 console.log 格式化引數後丟棄(不用 quiet: 其不格式化引數而測不出)
2975
+ }
2976
+ let done = 0
2977
+ try {
2978
+ for (let { kind, proc } of getKinds().filter((v) => KINDS.includes(v.kind))) {
2979
+ for (let [lab, type, mk] of items) {
2980
+ let modes = type === 'container' ? ['insert', 'save', 'del', 'mix.add', 'mix.del', 'mix.diff'] : ['insert', 'save', 'mix.add', 'mix.diff']
2981
+ for (let mode of modes) {
2982
+ let obj = mk()
2983
+ let n = Reflect.ownKeys(obj).length
2984
+ let input = () => {
2985
+ if (mode.startsWith('mix.')) {
2986
+ return { [mode.slice(4)]: type === 'container' ? obj : [obj] }
2987
+ }
2988
+ return type === 'container' ? obj : [obj]
2989
+ }
2990
+ let call = () => proc('u1', 'tests', mode.startsWith('mix.') ? 'mix' : mode, input())
2991
+ let r1 = await getReject(call())
2992
+ let r2 = await getReject(call())
2993
+ assert.strictEqual(typeof r1, 'string', `${kind} ${lab} ${mode} 第1次`)
2994
+ assert.strictEqual(r2, r1, `${kind} ${lab} ${mode} 同一物件第2次同結果`)
2995
+ assert.strictEqual(Reflect.ownKeys(obj).length, n, `${kind} ${lab} ${mode} 物件未被改動`)
2996
+ done++
2997
+ }
2998
+ assert.strictEqual(reads, 0, `${kind} ${lab} 診斷輸出與檢查不執行使用端程式`)
2999
+ }
3000
+ }
3001
+ }
3002
+ finally {
3003
+ console.log = logOrig
3004
+ }
3005
+ assert.strictEqual(done, 3 * (3 * 6 + 4), '完成數')
3006
+ })
3007
+
3008
+ })