aps-agent/apps/desktop/test/updater-smoke.mjs

161 lines
6.9 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.

#!/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);