161 lines
6.9 KiB
JavaScript
161 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 桌面端离线升级/回滚 冒烟脚本(纯逻辑,临时目录模拟,不启动 Electron、不真装 NSIS)。
|
||
* 运行:node test/updater-smoke.mjs
|
||
* 覆盖:校验和失败拒绝安装 / 版本回退拒绝 / 备份-安装-健康检查-失败自动回滚 / 审计留痕。
|
||
*/
|
||
import assert from 'node:assert/strict';
|
||
import { createHash } from 'node:crypto';
|
||
import fs from 'node:fs';
|
||
import http from 'node:http';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
|
||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||
const {
|
||
Updater,
|
||
UpgradeError,
|
||
verifyPackage,
|
||
} = await import(pathToFileURL(path.join(here, '..', 'updater.cjs')).href);
|
||
|
||
let passed = 0;
|
||
let failed = 0;
|
||
function check(name, fn) {
|
||
return Promise.resolve()
|
||
.then(fn)
|
||
.then(() => { passed += 1; console.log('PASS ' + name); })
|
||
.catch((error) => { failed += 1; console.log('FAIL ' + name + ' :: ' + error.message); });
|
||
}
|
||
|
||
function fixture() {
|
||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-updater-smoke-'));
|
||
const installDir = path.join(root, 'install');
|
||
const backupDir = path.join(root, 'backup');
|
||
const packageDir = path.join(root, 'upgrade');
|
||
const apsHome = path.join(root, '.aps');
|
||
fs.mkdirSync(path.join(installDir, 'resources'), { recursive: true });
|
||
fs.mkdirSync(packageDir, { recursive: true });
|
||
fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'old-exe-v0.1.0');
|
||
fs.writeFileSync(path.join(installDir, 'resources', 'index.html'), '<!doctype html>old');
|
||
return { root, installDir, backupDir, packageDir, apsHome };
|
||
}
|
||
|
||
function makeUpgrade(packageDir, { version = '0.2.0', payload = 'installer-v0.2.0', tamperHash = false } = {}) {
|
||
const fileName = 'aps-agent-desktop-' + version + '.exe';
|
||
const packagePath = path.join(packageDir, fileName);
|
||
fs.writeFileSync(packagePath, payload);
|
||
const manifest = {
|
||
schemaVersion: 1,
|
||
product: 'aps-agent-desktop',
|
||
version,
|
||
platform: process.platform,
|
||
package: {
|
||
fileName,
|
||
sha256: createHash('sha256').update(payload).digest('hex'),
|
||
size: Buffer.byteLength(payload),
|
||
},
|
||
releasedAt: new Date().toISOString(),
|
||
};
|
||
if (tamperHash) manifest.package.sha256 = '0'.repeat(64);
|
||
const manifestPath = path.join(packageDir, 'upgrade-manifest.json');
|
||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||
return { manifestPath, packagePath, manifest };
|
||
}
|
||
|
||
function startHealthServer(version = '0.2.0') {
|
||
return new Promise((resolve) => {
|
||
const server = http.createServer((req, res) => {
|
||
if (req.url === '/api/health') {
|
||
res.writeHead(200, { 'content-type': 'application/json' });
|
||
res.end(JSON.stringify({ ok: true, version }));
|
||
return;
|
||
}
|
||
res.writeHead(404);
|
||
res.end();
|
||
});
|
||
server.listen(0, '127.0.0.1', () => resolve({ server, base: 'http://127.0.0.1:' + server.address().port }));
|
||
});
|
||
}
|
||
|
||
const quiet = { info() {}, error() {}, warn() {} };
|
||
|
||
// ① 校验和失败 → 拒绝安装(安装目录不被触碰)
|
||
await check('checksum mismatch rejects install', async () => {
|
||
const fx = fixture();
|
||
try {
|
||
const { manifestPath, packagePath, manifest } = makeUpgrade(fx.packageDir, { tamperHash: true });
|
||
let rejected = false;
|
||
try {
|
||
await verifyPackage({ manifest, packagePath });
|
||
} catch (error) {
|
||
rejected = error instanceof UpgradeError && error.code === 'CHECKSUM_MISMATCH';
|
||
}
|
||
assert.equal(rejected, true, 'expected CHECKSUM_MISMATCH');
|
||
assert.equal(fs.readFileSync(path.join(fx.installDir, '工业智核 APS.exe'), 'utf8'), 'old-exe-v0.1.0');
|
||
} finally { fs.rmSync(fx.root, { recursive: true, force: true }); }
|
||
});
|
||
|
||
// ② 版本回退 → 拒绝安装
|
||
await check('version downgrade rejects install', async () => {
|
||
const fx = fixture();
|
||
try {
|
||
const { manifestPath, packagePath } = makeUpgrade(fx.packageDir, { version: '0.0.9' });
|
||
const updater = new Updater({ apsHome: fx.apsHome, logger: quiet });
|
||
await assert.rejects(
|
||
updater.applyUpdate({ manifestPath, packagePath, installDir: fx.installDir, backupDir: fx.backupDir, currentVersion: '0.1.0', install: async () => { throw new Error('must not install'); } }),
|
||
(error) => error.code === 'VERSION_NOT_NEWER',
|
||
);
|
||
} finally { fs.rmSync(fx.root, { recursive: true, force: true }); }
|
||
});
|
||
|
||
// ③ 成功升级:校验 → 备份 → 安装 → 健康检查 → 审计
|
||
await check('upgrade success with health check + audit', async () => {
|
||
const fx = fixture();
|
||
const { server, base } = await startHealthServer('0.2.0');
|
||
try {
|
||
const { manifestPath, packagePath } = makeUpgrade(fx.packageDir);
|
||
const updater = new Updater({ apsHome: fx.apsHome, logger: quiet });
|
||
const report = await updater.applyUpdate({
|
||
manifestPath, packagePath,
|
||
installDir: fx.installDir, backupDir: fx.backupDir,
|
||
currentVersion: '0.1.0', healthBase: base, expectedVersion: '0.2.0',
|
||
install: async () => fs.writeFileSync(path.join(fx.installDir, '工业智核 APS.exe'), 'new-exe-v0.2.0'),
|
||
});
|
||
assert.equal(report.ok, true);
|
||
assert.equal(fs.readFileSync(path.join(fx.installDir, '工业智核 APS.exe'), 'utf8'), 'new-exe-v0.2.0');
|
||
const audit = fs.readFileSync(path.join(fx.apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8');
|
||
assert.ok(audit.includes('upgrade.succeeded'));
|
||
} finally { server.close(); fs.rmSync(fx.root, { recursive: true, force: true }); }
|
||
});
|
||
|
||
// ④ 失败自动回滚:健康检查失败 → 文件恢复 + 审计记录原因
|
||
await check('failure auto-rollback restores files + audits reason', async () => {
|
||
const fx = fixture();
|
||
const { server, base } = await startHealthServer('0.2.0');
|
||
try {
|
||
const { manifestPath, packagePath } = makeUpgrade(fx.packageDir);
|
||
const updater = new Updater({ apsHome: fx.apsHome, logger: quiet });
|
||
await assert.rejects(
|
||
updater.applyUpdate({
|
||
manifestPath, packagePath,
|
||
installDir: fx.installDir, backupDir: fx.backupDir,
|
||
currentVersion: '0.1.0', healthBase: base, expectedVersion: '0.3.0',
|
||
install: async () => {
|
||
fs.writeFileSync(path.join(fx.installDir, '工业智核 APS.exe'), 'broken');
|
||
fs.writeFileSync(path.join(fx.installDir, 'resources', 'index.html'), 'broken');
|
||
},
|
||
}),
|
||
(error) => error.code === 'HEALTH_VERSION_MISMATCH',
|
||
);
|
||
assert.equal(fs.readFileSync(path.join(fx.installDir, '工业智核 APS.exe'), 'utf8'), 'old-exe-v0.1.0');
|
||
assert.equal(fs.readFileSync(path.join(fx.installDir, 'resources', 'index.html'), 'utf8'), '<!doctype html>old');
|
||
const audit = fs.readFileSync(path.join(fx.apsHome, 'logs', 'upgrade-audit.jsonl'), 'utf8');
|
||
assert.ok(audit.includes('upgrade.failed'));
|
||
assert.ok(audit.includes('rollback.done'));
|
||
} finally { server.close(); fs.rmSync(fx.root, { recursive: true, force: true }); }
|
||
});
|
||
|
||
console.log('\nupdater-smoke: ' + passed + ' passed, ' + failed + ' failed');
|
||
process.exit(failed === 0 ? 0 : 1);
|