Verify update artifacts with exact metadata and SHA-512, require host-confirmed upload completion before cleanup, harden credentials and backups, improve queue recovery and skipped-state reporting, expand Windows path coverage, and add CI packaging checks.
This commit is contained in:
@@ -3,9 +3,10 @@ const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const crypto = require('node:crypto');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
|
||||
const { isNewer, resolveReleaseVersion, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate } = require('../lib/updater');
|
||||
const { isNewer, resolveReleaseVersion, fetchGithubReleaseNotes, prepareUpdate, launchPreparedUpdate, pickSetupAsset, parseLatestYml } = require('../lib/updater');
|
||||
const releasePlanUrl = pathToFileURL(path.resolve(__dirname, '../scripts/release-plan.mjs')).href;
|
||||
|
||||
test('bridge title resolves product version instead of transport tag', () => {
|
||||
@@ -53,21 +54,28 @@ test('update preparation writes a verified installer without launching it', asyn
|
||||
assetUrl: 'https://update.invalid/setup.exe',
|
||||
assetName: 'setup.exe',
|
||||
assetSize: installer.length,
|
||||
latestYmlUrl: null
|
||||
remoteVersion: '2.2.0',
|
||||
latestYmlUrl: 'https://update.invalid/latest.yml'
|
||||
},
|
||||
tempDir,
|
||||
fetchImpl: async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
getReader: () => ({
|
||||
read: async () => {
|
||||
reads++;
|
||||
return reads === 1 ? { done: false, value: installer } : { done: true };
|
||||
fetchImpl: async url => url.endsWith('latest.yml')
|
||||
? {
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => `version: 2.2.0\npath: setup.exe\nsha512: ${crypto.createHash('sha512').update(installer).digest('base64')}\nsize: ${installer.length}\n`
|
||||
}
|
||||
: {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
getReader: () => ({
|
||||
read: async () => {
|
||||
reads++;
|
||||
return reads === 1 ? { done: false, value: installer } : { done: true };
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(prepared.installerPath, path.join(tempDir, 'setup.exe'));
|
||||
@@ -78,6 +86,76 @@ test('update preparation writes a verified installer without launching it', asyn
|
||||
}
|
||||
});
|
||||
|
||||
test('update preparation fails closed when checksum metadata is unavailable', async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-updater-test-'));
|
||||
try {
|
||||
await assert.rejects(
|
||||
prepareUpdate(null, {
|
||||
checkResult: {
|
||||
available: true,
|
||||
assetUrl: 'https://update.invalid/setup.exe',
|
||||
assetName: 'setup.exe',
|
||||
assetSize: 128 * 1024,
|
||||
remoteVersion: '2.2.0',
|
||||
latestYmlUrl: null
|
||||
},
|
||||
tempDir,
|
||||
fetchImpl: async () => assert.fail('installer download must not start without checksum metadata')
|
||||
}),
|
||||
/Prüfsummen-Metadaten fehlen/
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(tempDir, 'setup.exe')), false);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('setup selection never falls back to a portable executable', () => {
|
||||
const portable = { name: 'Multi-Hoster-Upload 2.2.0.exe' };
|
||||
assert.equal(pickSetupAsset([portable], '2.2.0'), null);
|
||||
assert.deepEqual(
|
||||
pickSetupAsset([portable, { name: 'Multi-Hoster-Upload Setup 2.2.0.exe' }], '2.2.0'),
|
||||
{ name: 'Multi-Hoster-Upload Setup 2.2.0.exe' }
|
||||
);
|
||||
});
|
||||
|
||||
test('checksum metadata must match the selected version, installer name, size, and SHA-512 shape', async () => {
|
||||
const sha = crypto.randomBytes(64).toString('base64');
|
||||
const fetchImpl = async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => `version: 2.2.0\npath: Multi-Hoster-Upload Setup 2.2.0.exe\nsha512: ${sha}\nsize: 456\n`
|
||||
});
|
||||
const metadata = await parseLatestYml('https://update.invalid/latest.yml', {
|
||||
version: '2.2.0',
|
||||
assetName: 'Multi-Hoster-Upload Setup 2.2.0.exe',
|
||||
assetSize: 456
|
||||
}, fetchImpl);
|
||||
|
||||
assert.deepEqual(metadata, {
|
||||
version: '2.2.0',
|
||||
path: 'Multi-Hoster-Upload Setup 2.2.0.exe',
|
||||
size: 456,
|
||||
sha512: sha
|
||||
});
|
||||
});
|
||||
|
||||
test('checksum metadata rejects a path that belongs to another artifact', async () => {
|
||||
const sha = crypto.randomBytes(64).toString('base64');
|
||||
await assert.rejects(
|
||||
parseLatestYml('https://update.invalid/latest.yml', {
|
||||
version: '2.2.0',
|
||||
assetName: 'Multi-Hoster-Upload Setup 2.2.0.exe',
|
||||
assetSize: 456
|
||||
}, async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => `version: 2.2.0\npath: Multi-Hoster-Upload 2.2.0.exe\nsha512: ${sha}\nsize: 456\n`
|
||||
})),
|
||||
/gehören nicht zum ausgewählten Installer/
|
||||
);
|
||||
});
|
||||
|
||||
test('a prepared installer launches at most once', () => {
|
||||
const calls = [];
|
||||
const child = { unrefCalls: 0, unref() { this.unrefCalls++; } };
|
||||
@@ -120,7 +198,7 @@ test('release plan keeps product artifacts separate from the transport tag', asy
|
||||
'Multi-Hoster-Upload Setup 2.0.7.exe.blockmap',
|
||||
'latest.yml'
|
||||
],
|
||||
latestYml: "version: 2.0.7\nfiles:\n - url: Multi-Hoster-Upload.Setup.2.0.7.exe\n sha512: abc123\n size: 456\npath: Multi-Hoster-Upload.Setup.2.0.7.exe\nsha512: abc123\nreleaseDate: '2026-08-07T12:00:00.000Z'\n"
|
||||
latestYml: "version: 2.0.7\nfiles:\n - url: Multi-Hoster-Upload Setup 2.0.7.exe\n sha512: abc123\n size: 456\npath: Multi-Hoster-Upload Setup 2.0.7.exe\nsha512: abc123\nreleaseDate: '2026-08-07T12:00:00.000Z'\n"
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user