205 lines
8.2 KiB
JavaScript
205 lines
8.2 KiB
JavaScript
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import fs from 'node:fs';
|
||
|
|
import os from 'node:os';
|
||
|
|
import path from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
|
||
|
|
import {
|
||
|
|
parseArgs,
|
||
|
|
assertSafeTempDir,
|
||
|
|
expectedSha256FromManifest,
|
||
|
|
findInstaller,
|
||
|
|
findUninstaller,
|
||
|
|
verifyInstallLayout,
|
||
|
|
listContents,
|
||
|
|
} from '../../scripts/nsis-install-smoke.mjs';
|
||
|
|
import { sha256File } from '../../scripts/offline-install-check.mjs';
|
||
|
|
|
||
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||
|
|
|
||
|
|
function tempRoot() {
|
||
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'aps-nsis-test-'));
|
||
|
|
}
|
||
|
|
|
||
|
|
test('nsis-smoke: parseArgs accepts positional installer and flags', () => {
|
||
|
|
const args = parseArgs([
|
||
|
|
'C:/tmp/aps-agent-desktop-0.1.0.exe',
|
||
|
|
'--run-uninstaller',
|
||
|
|
'--launch',
|
||
|
|
'--launch-seconds', '6',
|
||
|
|
'--timeout', '120000',
|
||
|
|
'--keep-temp-dir',
|
||
|
|
'--expected-sha256', 'b'.repeat(64),
|
||
|
|
]);
|
||
|
|
assert.equal(args.installer, path.resolve('C:/tmp/aps-agent-desktop-0.1.0.exe'));
|
||
|
|
assert.equal(args.runUninstaller, true);
|
||
|
|
assert.equal(args.launch, true);
|
||
|
|
assert.equal(args.launchSeconds, 6);
|
||
|
|
assert.equal(args.timeoutMs, 120000);
|
||
|
|
assert.equal(args.keepTempDir, true);
|
||
|
|
assert.equal(args.expectedSha256, 'b'.repeat(64));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: --no-launch is the explicit default', () => {
|
||
|
|
assert.equal(parseArgs(['--launch', '--no-launch']).launch, false);
|
||
|
|
assert.equal(parseArgs([]).launch, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: parseArgs rejects bad hash and unknown options', () => {
|
||
|
|
assert.throws(() => parseArgs(['--expected-sha256', 'zz']));
|
||
|
|
assert.throws(() => parseArgs(['--nope']));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: assertSafeTempDir accepts only own prefixed dirs under the temp root', () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const own = fs.mkdtempSync(path.join(root, 'aps-nsis-smoke-'));
|
||
|
|
const guarded = assertSafeTempDir(own, root);
|
||
|
|
assert.equal(guarded.resolved, path.resolve(own));
|
||
|
|
assert.ok(guarded.relative.startsWith('aps-nsis-smoke-'));
|
||
|
|
assert.throws(() => assertSafeTempDir(root, root), /temp root itself/);
|
||
|
|
assert.throws(() => assertSafeTempDir(path.join(os.tmpdir(), 'elsewhere'), root), /outside/);
|
||
|
|
const wrongPrefix = fs.mkdtempSync(path.join(root, 'other-prefix-'));
|
||
|
|
assert.throws(() => assertSafeTempDir(wrongPrefix, root), /prefix/);
|
||
|
|
assert.throws(() => assertSafeTempDir(path.join(root, '..', 'escape'), root), /outside/);
|
||
|
|
fs.rmSync(wrongPrefix, { recursive: true, force: true });
|
||
|
|
fs.rmSync(own, { recursive: true, force: true });
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: verifyInstallLayout validates main-exe electron-builder layout', () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const installDir = path.join(root, 'aps-nsis-smoke-layout');
|
||
|
|
fs.mkdirSync(path.join(installDir, 'resources', 'sidecar'), { recursive: true });
|
||
|
|
fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'x');
|
||
|
|
fs.writeFileSync(path.join(installDir, 'resources', 'app.asar'), 'x');
|
||
|
|
fs.writeFileSync(path.join(installDir, 'resources', 'sidecar', 'aps-sidecar.exe'), 'x');
|
||
|
|
fs.writeFileSync(path.join(installDir, 'Uninstall 工业智核 APS.exe'), 'x');
|
||
|
|
const layout = verifyInstallLayout(installDir, '工业智核 APS');
|
||
|
|
assert.equal(layout.valid, true);
|
||
|
|
assert.equal(layout.mainExe, true);
|
||
|
|
assert.equal(layout.appAsar, true);
|
||
|
|
assert.equal(layout.sidecar, true);
|
||
|
|
assert.equal(path.basename(layout.uninstaller), 'Uninstall 工业智核 APS.exe');
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: verifyInstallLayout reports broken layouts', () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const dir = path.join(root, 'aps-nsis-smoke-broken');
|
||
|
|
fs.mkdirSync(dir, { recursive: true });
|
||
|
|
fs.writeFileSync(path.join(dir, '工业智核 APS.exe'), 'x');
|
||
|
|
const layout = verifyInstallLayout(dir, '工业智核 APS');
|
||
|
|
assert.equal(layout.valid, false);
|
||
|
|
assert.ok(layout.reasons.length > 0);
|
||
|
|
assert.equal(verifyInstallLayout(path.join(root, 'nope'), '工业智核 APS').valid, false);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: expectedSha256FromManifest reads the nsis-installer entry', () => {
|
||
|
|
const hash = expectedSha256FromManifest({ entries: [{ id: 'nsis-installer', sha256: 'CD'.repeat(32) }] });
|
||
|
|
assert.equal(hash, 'cd'.repeat(32));
|
||
|
|
assert.equal(expectedSha256FromManifest(null), null);
|
||
|
|
assert.equal(expectedSha256FromManifest({ entries: [] }), null);
|
||
|
|
assert.equal(expectedSha256FromManifest({ entries: [{ id: 'other', sha256: 'a'.repeat(64) }] }), null);
|
||
|
|
assert.equal(expectedSha256FromManifest({ entries: [{ id: 'nsis-installer', sha256: 'not-hex' }] }), null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: findInstaller resolves explicit paths only', () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const missing = findInstaller({ root, explicit: path.join(root, 'nope.exe'), minBytes: 1 });
|
||
|
|
assert.equal(missing.exists, false);
|
||
|
|
assert.deepEqual(missing.candidates, [path.join(root, 'nope.exe')]);
|
||
|
|
const file = path.join(root, 'fake.exe');
|
||
|
|
fs.writeFileSync(file, Buffer.alloc(1024, 1));
|
||
|
|
const found = findInstaller({ root, explicit: file, minBytes: 1 });
|
||
|
|
assert.equal(found.exists, true);
|
||
|
|
assert.equal(found.source, 'explicit');
|
||
|
|
assert.equal(found.bytes, 1024);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: findInstaller uses the manifest entry when present', () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const releaseDir = path.join(root, 'apps', 'desktop', 'release');
|
||
|
|
fs.mkdirSync(releaseDir, { recursive: true });
|
||
|
|
const file = path.join(releaseDir, 'aps-agent-desktop-0.1.0.exe');
|
||
|
|
fs.writeFileSync(file, Buffer.alloc(2048, 2));
|
||
|
|
const manifest = { baseDir: root, entries: [{ id: 'nsis-installer', file: 'apps/desktop/release/aps-agent-desktop-0.1.0.exe' }] };
|
||
|
|
const found = findInstaller({ root, manifest, minBytes: 1 });
|
||
|
|
assert.equal(found.exists, true);
|
||
|
|
assert.equal(found.source, 'manifest');
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: findUninstaller matches the electron-builder naming', () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const dir = path.join(root, 'aps-nsis-smoke-u');
|
||
|
|
fs.mkdirSync(dir, { recursive: true });
|
||
|
|
assert.equal(findUninstaller(dir, '工业智核 APS'), null);
|
||
|
|
fs.writeFileSync(path.join(dir, 'Uninstall 工业智核 APS.exe'), 'x');
|
||
|
|
assert.equal(path.basename(findUninstaller(dir, '工业智核 APS')), 'Uninstall 工业智核 APS.exe');
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: listContents walks nested files and sorts by size', () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const dir = path.join(root, 'aps-nsis-smoke-contents');
|
||
|
|
fs.mkdirSync(path.join(dir, 'resources', 'sidecar'), { recursive: true });
|
||
|
|
fs.writeFileSync(path.join(dir, 'small.bin'), Buffer.alloc(10, 1));
|
||
|
|
fs.writeFileSync(path.join(dir, 'resources', 'big.bin'), Buffer.alloc(120, 2));
|
||
|
|
fs.writeFileSync(path.join(dir, 'resources', 'sidecar', 'mid.bin'), Buffer.alloc(60, 3));
|
||
|
|
const result = listContents(dir, 2);
|
||
|
|
assert.equal(result.fileCount, 3);
|
||
|
|
assert.equal(result.totalBytes, 190);
|
||
|
|
assert.equal(result.top.length, 2);
|
||
|
|
assert.equal(result.top[0].bytes, 120);
|
||
|
|
assert.equal(result.top[1].bytes, 60);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: sha256File is deterministic and matches hex format', async () => {
|
||
|
|
const root = tempRoot();
|
||
|
|
try {
|
||
|
|
const file = path.join(root, 'sample.bin');
|
||
|
|
fs.writeFileSync(file, Buffer.alloc(4096, 7));
|
||
|
|
const first = await sha256File(file);
|
||
|
|
const second = await sha256File(file);
|
||
|
|
assert.equal(first, second);
|
||
|
|
assert.match(first, /^[0-9a-f]{64}$/);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(root, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('nsis-smoke: auto-discovery finds the real installer on this machine', (t) => {
|
||
|
|
const found = findInstaller({ root: repoRoot });
|
||
|
|
if (!found.exists) {
|
||
|
|
t.skip('NSIS installer not built on this machine');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
assert.ok(found.bytes >= 50 * 1024 * 1024);
|
||
|
|
assert.ok(found.file.endsWith('.exe'));
|
||
|
|
});
|