159 lines
6.1 KiB
JavaScript
159 lines
6.1 KiB
JavaScript
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.downloads = 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)
|
|
updater.downloadUpdate = async () => {
|
|
updater.downloads++
|
|
updater.emit('update-downloaded', { version: '0.1.1' })
|
|
}
|
|
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()
|
|
await controller.download()
|
|
assert.equal(controller.snapshot().status, 'disabled')
|
|
assert.equal(updater.checks, 0)
|
|
assert.equal(updater.downloads, 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 () => {
|
|
updater.emit('update-available', { version: '0.1.1' })
|
|
return {}
|
|
}
|
|
await controller.check()
|
|
updater.downloadUpdate = async () => { throw new Error('checksum mismatch') }
|
|
await controller.download()
|
|
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 {}
|
|
}
|
|
updater.downloadUpdate = async () => {
|
|
updater.downloads++
|
|
return new Promise((resolve) => { finish = resolve })
|
|
}
|
|
assert.equal(controller.install(), false)
|
|
await controller.download()
|
|
assert.equal(updater.downloads, 0)
|
|
await controller.check()
|
|
assert.equal(controller.snapshot().status, 'available')
|
|
assert.equal(updater.autoDownload, false)
|
|
assert.equal(updater.downloads, 0)
|
|
assert.equal(controller.install(), false)
|
|
const pending = controller.download()
|
|
await controller.download()
|
|
await controller.check()
|
|
assert.equal(updater.checks, 1)
|
|
assert.equal(updater.downloads, 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/)
|
|
assert.throws(() => handlers.get('capsule:updates:download')(event), /denied/)
|
|
frame.url = url
|
|
assert.equal(handlers.get('capsule:updates:status')(event).currentVersion, '0.1.0')
|
|
cleanup()
|
|
assert.equal(handlers.size, 0)
|
|
})
|