105 lines
4.8 KiB
JavaScript
105 lines
4.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import zlib from 'node:zlib';
|
|
import { ENTRY_SCRIPT_NAME, buildZip, planPackageLayout, resolveSetupFile } from '../services/packageBuilder.js';
|
|
|
|
// --- layout planning (pure) --------------------------------------------------
|
|
|
|
test('planPackageLayout places linked assets by role when no package path is set', () => {
|
|
const layout = planPackageLayout([
|
|
{ assetId: 'a1', originalName: 'setup.exe', linkRole: 'installer' },
|
|
{ assetId: 'a2', originalName: 'config.xml', linkRole: 'support-file' },
|
|
{ assetId: 'a3', originalName: 'icon.png', linkRole: 'icon' }
|
|
]);
|
|
assert.equal(layout.scriptPath, ENTRY_SCRIPT_NAME);
|
|
assert.deepEqual(layout.assets.map((a) => a.packagePath), ['Files/setup.exe', 'SupportFiles/config.xml', 'Assets/icon.png']);
|
|
assert.ok(layout.directories.includes('Assets'), 'Assets dir added because an icon is present');
|
|
});
|
|
|
|
test('planPackageLayout honors an explicit package path verbatim', () => {
|
|
const layout = planPackageLayout([
|
|
{ assetId: 'a1', originalName: 'setup.exe', linkRole: 'installer', packagePath: 'Files/x64/setup.exe' }
|
|
]);
|
|
assert.equal(layout.assets[0].packagePath, 'Files/x64/setup.exe');
|
|
});
|
|
|
|
test('planPackageLayout has no Assets dir when nothing maps there', () => {
|
|
const layout = planPackageLayout([{ assetId: 'a1', originalName: 'setup.exe', linkRole: 'installer' }]);
|
|
assert.deepEqual(layout.directories, ['Files', 'SupportFiles']);
|
|
});
|
|
|
|
test('planPackageLayout drops colliding targets with a warning', () => {
|
|
const layout = planPackageLayout([
|
|
{ assetId: 'a1', originalName: 'setup.exe', linkRole: 'installer', packagePath: 'Files/setup.exe' },
|
|
{ assetId: 'a2', originalName: 'other.exe', linkRole: 'installer', packagePath: 'files/SETUP.exe' }
|
|
]);
|
|
assert.equal(layout.assets.length, 1, 'second asset on the same path is skipped');
|
|
assert.equal(layout.warnings.length, 1);
|
|
});
|
|
|
|
test('planPackageLayout relocates a bare entry-script name under its role folder, never overwriting root', () => {
|
|
const layout = planPackageLayout([
|
|
{ assetId: 'a1', originalName: 'Invoke-AppDeployToolkit.ps1', linkRole: 'reference', packagePath: 'Invoke-AppDeployToolkit.ps1' }
|
|
]);
|
|
assert.equal(layout.assets[0].packagePath, 'SupportFiles/Invoke-AppDeployToolkit.ps1');
|
|
assert.notEqual(layout.assets[0].packagePath, ENTRY_SCRIPT_NAME);
|
|
});
|
|
|
|
// --- setup file resolution (pure) --------------------------------------------
|
|
|
|
test('resolveSetupFile prefers an explicit choice, then installer role, then first Files entry', () => {
|
|
const layout = planPackageLayout([
|
|
{ assetId: 'a1', originalName: 'helper.exe', linkRole: 'package-file' },
|
|
{ assetId: 'a2', originalName: 'setup.exe', linkRole: 'installer' }
|
|
]);
|
|
assert.equal(resolveSetupFile(layout, 'Files/manual.exe'), 'Files/manual.exe');
|
|
assert.equal(resolveSetupFile(layout), 'Files/setup.exe');
|
|
|
|
const noInstaller = planPackageLayout([{ assetId: 'a1', originalName: 'helper.exe', linkRole: 'package-file' }]);
|
|
assert.equal(resolveSetupFile(noInstaller), 'Files/helper.exe');
|
|
|
|
const empty = planPackageLayout([]);
|
|
assert.equal(resolveSetupFile(empty), '');
|
|
});
|
|
|
|
// --- zip writer (pure, round-trips through zlib) -----------------------------
|
|
|
|
test('buildZip produces a valid archive whose entries inflate back to their input', () => {
|
|
const script = 'Write-Output "hello"\n'.repeat(50);
|
|
const binary = Buffer.from([0, 1, 2, 3, 255, 254, 253]);
|
|
const zip = buildZip([
|
|
{ path: ENTRY_SCRIPT_NAME, data: Buffer.from(script, 'utf8') },
|
|
{ path: 'Files', isDirectory: true },
|
|
{ path: 'Files/payload.bin', data: binary }
|
|
]);
|
|
|
|
assert.equal(zip.readUInt32LE(0), 0x04034b50, 'starts with a local file header');
|
|
const recovered = readZip(zip);
|
|
assert.equal(recovered.get(ENTRY_SCRIPT_NAME).toString('utf8'), script);
|
|
assert.ok(recovered.has('Files/'), 'directory entry is present');
|
|
assert.deepEqual([...recovered.get('Files/payload.bin')], [...binary]);
|
|
});
|
|
|
|
test('buildZip stores incompressible/empty data without inflating it', () => {
|
|
const zip = buildZip([{ path: 'a.txt', data: Buffer.alloc(0) }]);
|
|
assert.equal(readZip(zip).get('a.txt').length, 0);
|
|
});
|
|
|
|
// Minimal central-directory reader for the test: walk local headers and inflate.
|
|
function readZip(buf) {
|
|
const out = new Map();
|
|
let pos = 0;
|
|
while (buf.readUInt32LE(pos) === 0x04034b50) {
|
|
const method = buf.readUInt16LE(pos + 8);
|
|
const compSize = buf.readUInt32LE(pos + 18);
|
|
const nameLen = buf.readUInt16LE(pos + 26);
|
|
const extraLen = buf.readUInt16LE(pos + 28);
|
|
const name = buf.slice(pos + 30, pos + 30 + nameLen).toString('utf8');
|
|
const dataStart = pos + 30 + nameLen + extraLen;
|
|
const stored = buf.slice(dataStart, dataStart + compSize);
|
|
out.set(name, method === 8 ? zlib.inflateRawSync(stored) : stored);
|
|
pos = dataStart + compSize;
|
|
}
|
|
return out;
|
|
}
|