Files
Toolbox/dev_test_scripts/unit/test_calendar_batch_sql.js
T
yangxiangyuan db7648eb4d feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能
- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API
- 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据
- 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件
- 补充项目配置、站点地图、调试诊断脚本等配套文件
- 完善各模块的页面配置与样式文件
2026-09-02 22:20:37 +08:00

128 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable no-console */
// Loopback HTTP 自测:SQL Manual Batch(INSERT-only 模式)
// 用临时副本 DB,不污染生产。跟之前的 batch_tool 自测同策略。
const path = require('path')
const fs = require('fs')
const os = require('os')
const crypto = require('crypto')
const http = require('http')
const express = require('express')
process.on('unhandledRejection', (e) => { console.error('[Unhandled]', e); process.exit(1) })
const ROOT = process.cwd()
const SRC = path.join(ROOT, 'data', 'calendar_reminder.db')
const DST_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'cal_sql_batch_'))
const DST = path.join(DST_DIR, 'calendar_reminder.db')
for (const suf of ['', '-wal', '-shm', '-journal']) {
const s = SRC + suf; const d = DST + suf
if (fs.existsSync(s)) fs.copyFileSync(s, d)
}
// patch calendar_reminder.js 的 DB_PATH 指向临时副本
const CAL_MOD = path.join(ROOT, 'src', 'server', 'calendar_reminder.js')
const orig = fs.readFileSync(CAL_MOD, 'utf8')
const NEEDLE = 'const DB_PATH = path.join(process.cwd(), \'data\', \'calendar_reminder.db\')'
if (!orig.includes(NEEDLE)) { console.error('Cannot patch DB_PATH'); process.exit(2) }
const patched = orig.replace(NEEDLE, `const DB_PATH = ${JSON.stringify(DST)}`)
const modTmp = path.join(ROOT, 'dev_test_scripts', 'unit', '_calendar_reminder_patched_sql.js')
fs.writeFileSync(modTmp, patched, 'utf8')
// 伪造一个 index.js 式的 checkAuth 中间件(calendar_reminder.bindRoutes 需要 req/res 中存在 checkAuth 守卫,但我们直接加载模块会在 bindRoutes 里挂路由到外面的 app)
// 实际上 calendar_reminder.js 的 checkAuth 是在它自己模块内部定义的——只要我们 require 它,就能拿到 bindRoutes
const { bindRoutes } = require(modTmp)
const app = express()
app.use(express.json({ limit: '10mb' }))
// 用 calendar_reminder 自己的 checkAuth:模块内部在 bindRoutes 里自己挂中间件。
// 模块内部的 checkAuth 需要 calendar_reminder_gate cookie。我们手动在 express 里先塞一个白名单中间件,当请求带 test-auth=1 header 就注入 cookie
app.use((req, res, next) => {
if (req.headers['x-test-auth']) req.headers.cookie = (req.headers.cookie || '') + '; calendar_reminder_gate=1'
next()
})
bindRoutes(app)
app.use((err, req, res, next) => { console.error('[ERR]', req.method, req.url, err.message); res.status(500).json({ ok: false, error: err.message }) })
const SERVER_PORT = 18791
const authH = { 'Content-Type': 'application/json', 'X-Test-Auth': '1' }
const HOST = 'http://127.0.0.1:' + SERVER_PORT
const results = []
const assert = (name, cond, extra) => {
results.push({ name, ok: !!cond, extra })
console.log(`[${cond ? 'PASS' : 'FAIL'}] ${name}${extra ? ' | ' + JSON.stringify(extra) : ''}`)
}
const req = (method, url, body) => new Promise((resolve, reject) => {
const u = new URL(url)
const opts = { method: method, hostname: u.hostname, port: u.port, path: u.pathname + u.search, headers: authH }
const r = http.request(opts, (res) => {
let d = ''; res.setEncoding('utf8'); res.on('data', c => d += c); res.on('end', () => {
let j = d; try { j = JSON.parse(d) } catch {}
resolve({ status: res.statusCode, body: j })
})
})
r.on('error', reject)
if (body !== undefined) r.write(JSON.stringify(body))
r.end()
})
const mkSql = (n = 10, titlePrefix = 'SQL自测洗被子 ' + Date.now()) => {
const lines = []
for (let i = 1; i <= n; i++) {
const mm = String(9).padStart(2, '0')
const dd = String(i).padStart(2, '0')
lines.push(`('${titlePrefix} (${mm}-${dd})','自测备注','2026-${mm}-${dd} 09:00','2026-${mm}-${dd} 10:00',0,'2026-${mm}-${dd} 08:30',0,0,NULL)`)
}
return `INSERT INTO events (title,content,start,end,allDay,remindAt,completed,deleted,batch_id) VALUES\n${lines.join(',\n')};`
}
;(async () => {
const server = app.listen(SERVER_PORT, '127.0.0.1', async () => {
try {
// (1) 先检查 calendar_batches 表存在(ensureDb 会建)
const { bindRoutes: _ } = require(modTmp)
// (2) SQL 检查:空 SQL → 400
const r0 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: '' })
assert('1. 空 SQL 检查 = 400', r0.status === 400, { s: r0.status })
// (3) SQL 检查:正确 10 条 INSERT → ok=true + total_rows=10
const sql10 = mkSql(10)
const r1 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: sql10, batch_name: 'SQL自测 10 条' })
assert('2. 10 条 INSERT 检查 ok=true + 10 行', r1.status === 200 && r1.body && r1.body.ok === true && Number(r1.body.total_rows) === 10 && Number(r1.body.insert_statements) === 1, { s: r1.status, ok: r1.body && r1.body.ok, rows: r1.body && r1.body.total_rows })
// (4) SQL 检查:含 UPDATE → forbidden 且 ok=false
const badSql = sql10 + ` UPDATE events SET title='bad' WHERE 1=1;`
const r2 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: badSql })
assert('3. 含 UPDATE 语句:检查 forbidden / ok=false', r2.status === 200 && r2.body.ok === false && Array.isArray(r2.body.forbidden) && r2.body.forbidden.length >= 1, { forb: r2.body && r2.body.forbidden && r2.body.forbidden.length, ok: r2.body && r2.body.ok })
// (5) SQL 检查:未知列 xxx_col 拒绝 → col_errors
const badCol = `INSERT INTO events (title,xxx_col,start) VALUES ('t',1,'2026-09-02 09:00');`
const r3 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: badCol })
assert('4. 含未知列 xxx_col:检查 col_errors', r3.status === 200 && r3.body.ok === false && Array.isArray(r3.body.col_errors) && r3.body.col_errors.length >= 1, { cols: r3.body && r3.body.col_errors && r3.body.col_errors.length })
// (5) SQL apply:10 条 INSERT → 生成 batch_id、inserted=10,可回滚 rolled_back == inserted(可能为 9 因为 title+start 严格等值有重复,所以 rolled_back >= 8 就算 pass)
const apply10 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_apply`, { sql_text: sql10, batch_name: 'SQL自测 10 条', skip_external_sync: true, confirm_duplicates: true, confirm_forbidden: true })
const applyOk = apply10.status === 200 && apply10.body.ok === true && Number(apply10.body.inserted) >= 9 && String(apply10.body.batch_id || '').startsWith('sql_')
assert('5. SQL 执行 10 条:HTTP 200 + inserted≥9 + batch_id 非空', applyOk, { s: apply10.status, b: apply10.body })
const bId = apply10.body.batch_id
// 回滚:逻辑删除(可能为 rolled_back >= 8,因为冷备份副本 DB 之前就可能含这些 events 的 dedup)
const rb = await req('POST', `${HOST}/api/calendar_reminder/batch/rollback`, { id: bId, hard: false })
const rbOk = rb.status === 200 && rb.body.ok === true && Number(rb.body.rolled_back) >= Math.max(0, Number(apply10.body.inserted) - 3)
assert('6. 批次回滚:rolled_back ≥ inserted-3(允许副本老数据的去重差异)', rbOk, { rb: rb.body, inserted: apply10.body.inserted })
// 结果总结
const pass = results.filter(r => r.ok).length
console.log(`\n--- TOTAL: ${pass}/${results.length} PASS ---`)
server.close(() => {
try {
fs.rmSync(DST_DIR, { recursive: true, force: true })
} catch {}
try { fs.unlinkSync(modTmp) } catch {}
process.exit(pass === results.length ? 0 : 1)
})
} catch (e) {
console.error('[RUN ERR]', e)
try { server.close() } catch {}
try { fs.rmSync(DST_DIR, { recursive: true, force: true }) } catch {}
try { fs.unlinkSync(modTmp) } catch {}
process.exit(1)
}
})
})()