import test from 'node:test' import assert from 'node:assert/strict' import { EventEmitter } from 'node:events' import { createUpdateController } from '../src/main/updateController.mjs' import { registerUpdateIpc, isTrustedUpdateSender } from '../src/main/updateIpc.mjs' function fixture(enabled = true) { const updater = new EventEmitter() const states = [] const scheduled = [] const cancelled = [] updater.checks = 0 updater.installs = [] updater.checkForUpdates = async () => { updater.checks++ updater.emit('update-not-available', { version: '0.1.0' }) return {} } updater.quitAndInstall = (...args) => updater.installs.push(args) const controller = createUpdateController({ updater, enabled, version: '0.1.0', onState: (state) => states.push(state), schedule: (callback, delay) => { const task = { callback, delay }; scheduled.push(task); return task }, cancel: (task) => cancelled.push(task) }) return { updater, controller, states, scheduled, cancelled } } test('local/dev builds never check, schedule, or install', async () => { const { controller, updater, scheduled } = fixture(false) controller.start() await controller.check() assert.equal(controller.snapshot().status, 'disabled') assert.equal(updater.checks, 0) assert.equal(scheduled.length, 0) assert.equal(controller.install(), false) }) test('release checks after launch and periodically, with cleanup', async () => { const { controller, updater, scheduled, cancelled } = fixture() controller.start() controller.start() assert.equal(scheduled.length, 1) assert.equal(scheduled[0].delay, 15000) await scheduled[0].callback() assert.equal(updater.checks, 1) assert.equal(controller.snapshot().status, 'current') assert.equal(scheduled[1].delay, 14400000) controller.dispose() assert.equal(cancelled[0], scheduled[1]) assert.equal(updater.listenerCount('download-progress'), 0) }) test('network and download failures are retryable without exposing raw errors', async () => { const { controller, updater } = fixture() updater.checkForUpdates = async () => { throw new Error('private endpoint or local path') } await controller.check() assert.equal(controller.snapshot().status, 'error') assert.doesNotMatch(JSON.stringify(controller.snapshot()), /private/) updater.checkForUpdates = async () => ({ downloadPromise: Promise.reject(new Error('checksum mismatch')) }) await controller.check() assert.equal(controller.snapshot().status, 'error') updater.checkForUpdates = async () => { updater.emit('update-not-available') return {} } await controller.check() assert.equal(controller.snapshot().status, 'current') }) test('one in-flight download, progress, verified readiness, and explicit restart', async () => { const { controller, updater } = fixture() let finish updater.checkForUpdates = async () => { updater.checks++ updater.emit('update-available', { version: '0.1.1' }) return { downloadPromise: new Promise((resolve) => { finish = resolve }) } } assert.equal(controller.install(), false) const pending = controller.check() await controller.check() assert.equal(updater.checks, 1) updater.emit('download-progress', { percent: 53.2 }) assert.equal(controller.snapshot().percent, 53) assert.equal(controller.install(), false) updater.emit('update-downloaded', { version: '0.1.1' }) finish() await pending assert.equal(updater.autoInstallOnAppQuit, false) assert.equal(updater.allowDowngrade, false) assert.equal(updater.allowPrerelease, false) assert.equal(updater.installs.length, 0) await controller.check() assert.equal(updater.checks, 1) assert.equal(controller.install(), true) assert.equal(controller.install(), false) assert.deepEqual(updater.installs, [[true, true]]) }) test('updater error event clears readiness and prevents installation', () => { const { controller, updater } = fixture() updater.emit('update-downloaded', { version: '0.1.1' }) updater.emit('error', new Error('verification failed')) assert.equal(controller.install(), false) assert.equal(controller.snapshot().status, 'error') }) test('update IPC rejects sites, child frames, and unowned windows', () => { const url = 'file:///C:/Capsule/out/renderer/index.html' const frame = { url: `${url}#/settings` } const sender = { mainFrame: frame } const windows = [{ webContents: sender, isDestroyed: () => false }] const event = { sender, senderFrame: frame } assert.equal(isTrustedUpdateSender(event, windows, url), true) assert.equal(isTrustedUpdateSender({ ...event, senderFrame: { url } }, windows, url), false) assert.equal(isTrustedUpdateSender(event, [], url), false) frame.url = 'https://ttp.joeykimsey.com/' assert.equal(isTrustedUpdateSender(event, windows, url), false) frame.url = `${url}?redirect=other` assert.equal(isTrustedUpdateSender(event, windows, url), false) const handlers = new Map() const { controller } = fixture() const cleanup = registerUpdateIpc({ ipcMain: { handle: (name, handler) => handlers.set(name, handler), removeHandler: (name) => handlers.delete(name) }, controller, getWindows: () => windows, rendererUrl: url }) assert.throws(() => handlers.get('capsule:updates:install')(event), /denied/) frame.url = url assert.equal(handlers.get('capsule:updates:status')(event).currentVersion, '0.1.0') cleanup() assert.equal(handlers.size, 0) })