62 lines
4.0 KiB
JavaScript
62 lines
4.0 KiB
JavaScript
// 服务端存储加固冒烟:空同步不落盘 / 墓碑GC / 大批删除前置快照 / 损坏隔离
|
|
const BASE = 'http://127.0.0.1:3780'
|
|
let pass = 0, fail = 0
|
|
function check(name, ok, detail) {
|
|
if (ok) { pass++; console.log(' PASS ' + name) } else { fail++; console.log(' FAIL ' + name + (detail ? ' -> ' + detail : '')) }
|
|
}
|
|
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
async function j(path, body, token) {
|
|
const r = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: 'Bearer ' + token } : {}) }, body: body ? JSON.stringify(body) : undefined })
|
|
return { status: r.status, json: await r.json().catch(() => ({})) }
|
|
}
|
|
async function main() {
|
|
const fsp = await import('fs')
|
|
let reg = await j('/api/auth/register', { username: 'storagetest', password: 'test123456' })
|
|
if (reg.status === 409) reg = await j('/api/auth/login', { username: 'storagetest', password: 'test123456' })
|
|
console.log('auth status:', reg.status)
|
|
const tok = reg.json.token
|
|
const file = 'server/data/users/' + reg.json.userId + '.json'
|
|
const row = { uuid: 'm1', title: 't', content: 'c', created_at: '2026-10-26T00:00:00', updated_at: '2026-10-26T00:00:00' }
|
|
const push = tables => j('/api/sync', { device: 't', tables, tombstones: [] }, tok)
|
|
|
|
const p1 = await push({ memo: [row] })
|
|
console.log('push1 status:', p1.status)
|
|
check('首次同步落盘(200+文件存在)', p1.status === 200 && fsp.existsSync(file))
|
|
const m1 = fsp.statSync(file).mtimeMs
|
|
|
|
await sleep(30)
|
|
const p2 = await push({ memo: [row] })
|
|
await sleep(30)
|
|
const m2 = fsp.statSync(file).mtimeMs
|
|
check('空推送不重写文件(脏检查)', p2.status === 200 && m2 === m1, 'mtime ' + m1 + ' -> ' + m2)
|
|
|
|
await j('/api/sync', { device: 't', tables: {}, tombstones: [{ table: 'memo', uuid: 'old1', deleted_at: '2026-09-01T00:00:00' }] }, tok)
|
|
const d = JSON.parse(fsp.readFileSync(file, 'utf8'))
|
|
check('过期墓碑(>14天)自动回收', !d.tombstones.memo || !d.tombstones.memo.old1, JSON.stringify(d.tombstones))
|
|
|
|
const dirBefore = fsp.readdirSync('server/data/backups').filter(x => /^\d{4}-\d{2}-\d{2}/.test(x))
|
|
const tombs = Array.from({ length: 20 }, (_, i) => ({ table: 'memo', uuid: 'ghost' + i, deleted_at: '2026-10-26T23:59:00' }))
|
|
tombs.push({ table: 'memo', uuid: 'm1', deleted_at: '2026-10-26T23:59:00' }) // m1 本尊:应被删除
|
|
await sleep(1100) // 错开秒级时间戳,避免 _t 目录同名覆盖
|
|
const bigResp = await j('/api/sync', { device: 't', tables: {}, tombstones: tombs }, tok)
|
|
console.log('big-tombstone status:', bigResp.status)
|
|
const dirAfter = fsp.readdirSync('server/data/backups').filter(x => /^\d{4}-\d{2}-\d{2}/.test(x))
|
|
console.log('backup dirs:', dirAfter.join(' | '))
|
|
check('大批删除触发前置快照', dirAfter.length > dirBefore.length, dirBefore.length + '->' + dirAfter.length)
|
|
const d2 = JSON.parse(fsp.readFileSync(file, 'utf8'))
|
|
check('墓碑应用后行已删除', !d2.tables.memo || !d2.tables.memo.m1, JSON.stringify(Object.keys(d2.tables.memo || {})))
|
|
check('墓碑已记录(待客户端收敛)', Object.keys(d2.tombstones.memo || {}).length >= 21, JSON.stringify(d2.tombstones.memo || {}))
|
|
|
|
fsp.writeFileSync(file, '{ this is not json !!!')
|
|
const r5 = await push({ memo: [{ uuid: 'm2', title: 'x', content: '', created_at: '2026-10-26T00:00:00', updated_at: '2026-10-26T00:00:00' }] })
|
|
check('损坏文件不再静默当空(同步仍200)', r5.status === 200, 'status=' + r5.status)
|
|
const quarantined = fsp.readdirSync('server/data/users').some(f => f.includes('.corrupt-'))
|
|
check('损坏文件已隔离保留原件', quarantined, fsp.readdirSync('server/data/users').join(','))
|
|
const d3 = JSON.parse(fsp.readFileSync('server/data/users/' + reg.json.userId + '.json', 'utf8'))
|
|
check('新纪元已写入(created_at 非空)', !!d3.created_at)
|
|
|
|
console.log('RESULT: ' + (fail === 0 ? 'ALL PASS ' + pass : 'FAIL ' + fail + '/' + (pass + fail)))
|
|
if (fail) process.exit(1)
|
|
}
|
|
main().catch(e => { console.error('SMOKE ERROR', e); process.exit(1) })
|