Files
Drsakura 1ffcc1ea07 feat: add admin web updates and verified username changes
Add isolated update jobs with database backups, progress logs and application rollback. Remove default account hints and prevent default admin recreation after renaming.

Enforce customer and order search scopes, secure session cookies, fix quotation rounding and amount wording, and update backend dependencies. Add API and deployment regression coverage.
2026-10-22 17:24:19 +08:00

77 lines
4.6 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const Database = require('better-sqlite3-multiple-ciphers');
const { executeUpdate } = require('../lib/updateWorker');
for (const mode of ['success', 'build-failure', 'restart-failure', 'health-failure', 'rollback-failure']) {
test(`web update deployment: ${mode}`, async t => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'happy-update-'));
let live;
t.after(() => {
if (live?.open) live.close();
fs.rmSync(root, { recursive: true, force: true });
});
const data = path.join(root, 'backend/data');
const dir = path.join(data, 'system-update');
const jobDir = path.join(dir, 'test-job');
const stage = path.join(jobDir, 'source');
const modules = path.join(root, 'backend/node_modules');
const dist = path.join(root, 'frontend/dist');
for (const folder of [jobDir, modules, dist]) fs.mkdirSync(folder, { recursive: true });
fs.writeFileSync(path.join(modules, 'marker'), 'old modules');
fs.writeFileSync(path.join(dist, 'index.html'), 'old page');
fs.writeFileSync(path.join(root, 'server-code'), 'old');
fs.writeFileSync(path.join(dir, 'status.json'), JSON.stringify({ state: 'queued' }));
fs.writeFileSync(path.join(dir, 'lock'), 'locked');
live = new Database(path.join(data, 'trade.db'));
live.pragma('journal_mode = WAL');
live.exec('CREATE TABLE assets (value TEXT); INSERT INTO assets VALUES (\'real business data\')');
const commands = [];
const hooks = {
run: async (command, args, cwd) => {
commands.push([command, ...args]);
if (command === 'git') {
if (args[0] === 'rev-parse') return args[1] === 'HEAD' ? 'old-commit' : 'new-commit';
if (args[0] === 'status') return '';
if (args[0] === 'worktree' && args[1] === 'add') {
for (const folder of ['backend/node_modules', 'frontend/dist']) fs.mkdirSync(path.join(stage, folder), { recursive: true });
fs.writeFileSync(path.join(stage, 'package.json'), JSON.stringify({ version: '2.0.0' }));
fs.writeFileSync(path.join(stage, 'backend/node_modules/marker'), 'new modules');
fs.writeFileSync(path.join(stage, 'frontend/dist/index.html'), 'new page');
}
if (args[0] === 'reset') fs.writeFileSync(path.join(root, 'server-code'), args[2] === 'old-commit' ? 'old' : 'new');
if (args[0] === 'worktree' && args[1] === 'remove') fs.rmSync(stage, { recursive: true, force: true });
}
if (command === 'npm' && args[0] === 'run' && mode === 'build-failure') throw new Error('build failed');
if (command === 'systemctl' && args[0] === 'restart' && ['restart-failure', 'rollback-failure'].includes(mode)) throw new Error('restart failed');
if (command === 'systemctl' && args[0] === 'start' && mode === 'rollback-failure') throw new Error('rollback failed');
return '';
},
backup: destination => live.backup(destination),
healthy: async expected => { if (expected && mode === 'health-failure') throw new Error('health check failed'); }
};
await executeUpdate({ root, data, dir, jobDir, service: 'erp', repo: 'owner/repo', tag: 'v2.0.0',
version: '2.0.0', node: process.execPath, port: '4300' }, hooks);
const status = JSON.parse(fs.readFileSync(path.join(dir, 'status.json'), 'utf8'));
const success = mode === 'success';
assert.equal(status.state, success ? 'completed' : 'failed');
assert.equal(fs.readFileSync(path.join(modules, 'marker'), 'utf8'), success ? 'new modules' : 'old modules');
assert.equal(fs.readFileSync(path.join(dist, 'index.html'), 'utf8'), success ? 'new page' : 'old page');
assert.equal(fs.readFileSync(path.join(root, 'server-code'), 'utf8'), success ? 'new' : 'old');
assert.equal(fs.existsSync(path.join(dir, 'lock')), false);
assert.equal(live.prepare('SELECT value FROM assets').get().value, 'real business data');
if (mode === 'build-failure') {
assert.ok(!commands.some(args => args[0] === 'systemctl' || args[1] === 'reset'));
} else {
const backup = new Database(path.join(jobDir, 'trade-before-update.db'), { readonly: true });
assert.equal(backup.prepare('SELECT value FROM assets').get().value, 'real business data');
backup.close();
}
if (['restart-failure', 'health-failure'].includes(mode)) assert.match(status.message, /已恢复上一版/);
if (mode === 'rollback-failure') assert.match(status.message, /自动恢复失败/);
});
}