import assert from 'node:assert/strict'; import { Buffer } from 'node:buffer'; import { signedFixture } from './updater-fixture.mjs'; import test from 'node:test'; import { compareVersions, updateManifest, verifyUpdateSignature, } from '../update-manifest.mjs'; import { updatePresentation } from '../ui/updates-state.js'; test('compares stable versions numerically and rejects prerelease or malformed versions', () => { assert.equal(compareVersions('1.10.0', '1.9.9'), 1); assert.equal(compareVersions('1.0.0', '2.0.0'), -1); assert.equal(compareVersions('1.0.0', '1.0.0'), 0); for (const invalid of [ 'v1.0.0', '1.0.0-beta', '01.0.0', '1.0.0+2', '', '1.2', ]) { assert.throws(() => compareVersions(invalid, '1.0.0')); } }); test('verifies minisign archive and trusted comment with the permanent key', () => { const bytes = Buffer.from('signed app'); const fixture = signedFixture(bytes); assert.doesNotThrow(() => verifyUpdateSignature(bytes, fixture.signature, fixture.publicKey), ); assert.throws( () => verifyUpdateSignature( Buffer.from('tampered app'), fixture.signature, fixture.publicKey, ), /signature/, ); assert.throws( () => verifyUpdateSignature( bytes, fixture.signature, signedFixture(bytes).publicKey, ), /signature/, ); for (const signature of [ '', 'invalid', Buffer.from( Buffer.from(fixture.signature, 'base64') .toString() .replace('timestamp:123456', 'timestamp:999999'), ).toString('base64'), ]) { assert.throws( () => verifyUpdateSignature(bytes, signature, fixture.publicKey), /signature/, ); } }); test('manifest selects the exact supported architecture and embeds release notes', () => { const metadata = { version: '1.2.3', target: 'aarch64-apple-darwin', updaterFilename: 'KanbanGit.app.tar.gz', }; assert.deepEqual( updateManifest( metadata, 'https://release.example/1.2.3', 'signed', 'Release notes', ), { version: '1.2.3', notes: 'Release notes', platforms: { 'darwin-aarch64': { url: 'https://release.example/1.2.3/KanbanGit.app.tar.gz', signature: 'signed', }, }, }, ); assert.throws(() => updateManifest( { ...metadata, target: 'android' }, 'https://example.com', 'signed', ), ); }); test('update controls prevent concurrent operations and allow failed downloads to retry', () => { for (const phase of [ 'checking', 'downloading', 'verifying', 'installing', 'restarting', ]) { const view = updatePresentation({ phase, version: '2.0.0' }); assert.equal(view.busy, true); assert.equal(view.canInstall, false); } assert.equal( updatePresentation({ phase: 'available', version: '2.0.0' }).canInstall, true, ); assert.equal( updatePresentation({ phase: 'error', version: '2.0.0' }).canInstall, true, ); assert.equal( updatePresentation({ phase: 'current', version: null }).canInstall, false, ); });