auto-update-manager.test.mjs 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { test } from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import { EventEmitter } from 'node:events'
  4. import { DesktopAutoUpdateManager } from './auto-update-manager.mjs'
  5. const deferred = () => { let resolve, reject; const promise = new Promise((yes, no) => { resolve = yes; reject = no }); return { promise, resolve, reject } }
  6. function fixture(t, options = {}) {
  7. const driver = new EventEmitter(), calls = []
  8. driver.checkForUpdates = async () => ({ isUpdateAvailable: true, updateInfo: { version: '0.4.63', releaseNotes: '<b>纯文本说明</b>' }, cancellationToken: { cancel() { calls.push('cancel') } } })
  9. driver.downloadUpdate = async () => { driver.emit('update-downloaded'); return ['update.zip'] }
  10. driver.quitAndInstall = () => calls.push('install')
  11. const manager = new DesktopAutoUpdateManager({ driver, version: '0.4.62', ...options })
  12. t.after(() => { manager.stopChecking(); clearTimeout(manager.stagingTimer); clearTimeout(manager.restartTimer) })
  13. return { manager, driver, calls }
  14. }
  15. test('checks app version, waits for native staging and installs only once after cleanup', async t => {
  16. const cleanup = deferred(), staged = deferred(), events = []
  17. const { manager, driver } = fixture(t, { prepareInstall: async () => { events.push('cleanup'); await cleanup.promise } })
  18. driver.downloadUpdate = () => { driver.emit('download-progress', { transferred: 10, total: 20 }); driver.emit('update-downloaded'); return staged.promise }
  19. driver.quitAndInstall = () => events.push('install')
  20. const checked = await manager.check()
  21. assert.equal(checked.available, true)
  22. assert.equal(checked.currentVersion, '0.4.62')
  23. assert.equal(driver.autoDownload, false)
  24. const download = manager.download()
  25. assert.equal(manager.snapshot().phase, 'preparing')
  26. assert.equal(manager.snapshot().canCancel, false)
  27. await manager.install(); await manager.check(); manager.cancel()
  28. assert.deepEqual(events, [])
  29. staged.resolve(['update.zip']); await download
  30. assert.equal(manager.snapshot().phase, 'ready')
  31. await manager.check(false)
  32. assert.equal(manager.snapshot().phase, 'ready')
  33. const install = manager.install()
  34. await manager.install()
  35. assert.deepEqual(events, ['cleanup'])
  36. cleanup.resolve(); await install
  37. assert.deepEqual(events, ['cleanup', 'install'])
  38. await manager.install(); assert.deepEqual(events, ['cleanup', 'install'])
  39. })
  40. test('no update and invalid environments never offer installation', async t => {
  41. const { manager, driver, calls } = fixture(t)
  42. driver.checkForUpdates = async () => ({ isUpdateAvailable: false, updateInfo: { version: '0.4.62' } })
  43. assert.equal((await manager.check()).available, false)
  44. await manager.download(); await manager.install(); assert.deepEqual(calls, [])
  45. driver.checkForUpdates = async () => null
  46. assert.equal((await manager.check()).phase, 'failed')
  47. })
  48. test('check failures cannot masquerade as up-to-date or erase a manual error', async t => {
  49. const { manager, driver } = fixture(t)
  50. driver.checkForUpdates = async () => { throw new Error('HTTP 404 latest-mac.yml') }
  51. const failed = await manager.check()
  52. assert.equal(failed.phase, 'failed'); assert.equal(failed.available, false)
  53. assert.match(failed.error, /尚未提供完整/)
  54. await manager.check(false); assert.deepEqual(manager.snapshot(), failed)
  55. driver.checkForUpdates = async () => { driver.emit('error', new Error('network')); return { isUpdateAvailable: false } }
  56. assert.equal((await manager.check()).phase, 'failed')
  57. })
  58. test('background failures remain silent and checks are single-flight', async t => {
  59. const pending = deferred(); const { manager, driver } = fixture(t)
  60. let calls = 0
  61. driver.checkForUpdates = () => { calls++; return pending.promise }
  62. const checking = manager.check(false)
  63. await manager.check(false); assert.equal(calls, 1)
  64. pending.reject(new Error('ENOTFOUND'))
  65. assert.equal((await checking).phase, 'idle')
  66. })
  67. test('manual checking promotes a pending background error to a visible failure', async t => {
  68. const { manager, driver } = fixture(t), pending = deferred()
  69. driver.checkForUpdates = () => pending.promise
  70. const checking = manager.check(false)
  71. await manager.check(true)
  72. pending.reject(new Error('ENOTFOUND'))
  73. assert.equal((await checking).phase, 'failed')
  74. })
  75. test('canceling a download requires a fresh check and can then recover', async t => {
  76. const { manager, driver } = fixture(t), pending = deferred()
  77. const normal = driver.downloadUpdate
  78. driver.downloadUpdate = () => pending.promise
  79. await manager.check()
  80. manager.token.cancel = () => pending.reject(new Error('cancelled'))
  81. const downloading = manager.download()
  82. assert.equal(manager.snapshot().canCancel, true)
  83. manager.cancel(); assert.equal((await downloading).phase, 'cancelled')
  84. driver.downloadUpdate = normal
  85. await manager.check(); assert.equal((await manager.download()).phase, 'ready')
  86. })
  87. test('download errors reset availability and allow a new check', async t => {
  88. const { manager, driver } = fixture(t)
  89. driver.downloadUpdate = async () => { const error = new Error('sha512 mismatch'); driver.emit('error', error); throw error }
  90. await manager.check(); const failed = await manager.download()
  91. assert.equal(failed.phase, 'failed'); assert.match(failed.error, /完整性/)
  92. assert.equal(failed.retryable, true)
  93. assert.equal((await manager.check()).available, true)
  94. })
  95. test('native failures and late ready events never offer retry or installation', async t => {
  96. const { manager, driver, calls } = fixture(t), pending = deferred()
  97. driver.downloadUpdate = () => { driver.emit('update-downloaded'); return pending.promise }
  98. await manager.check(); const downloading = manager.download()
  99. driver.emit('error', new Error('signature validation failed'))
  100. assert.equal(manager.snapshot().retryable, false)
  101. assert.match(manager.snapshot().error, /签名验证失败.*重新打开/)
  102. pending.resolve([]); await downloading
  103. driver.emit('update-downloaded'); driver.emit('download-progress', { transferred: 100, total: 100 })
  104. await manager.install(); await manager.check(); await manager.download()
  105. assert.equal(manager.snapshot().phase, 'failed'); assert.deepEqual(calls, [])
  106. })
  107. test('staging timeout stays failed after a late native completion', async t => {
  108. t.mock.timers.enable({ apis: ['setTimeout'] })
  109. const { manager, driver } = fixture(t, { stagingTimeout: 50 }), pending = deferred()
  110. driver.downloadUpdate = () => { driver.emit('update-downloaded'); return pending.promise }
  111. await manager.check(); const downloading = manager.download()
  112. t.mock.timers.tick(51)
  113. assert.equal(manager.snapshot().phase, 'failed')
  114. pending.resolve([]); await downloading
  115. assert.equal(manager.snapshot().phase, 'failed')
  116. })
  117. test('cleanup failure restores the app and never dispatches install', async t => {
  118. let restored = 0
  119. const { manager, calls } = fixture(t, { prepareInstall: async () => { throw new Error('cleanup failed') }, recoverInstall: async () => { restored++ } })
  120. await manager.check(); await manager.download(); await manager.install()
  121. assert.deepEqual(calls, []); assert.equal(restored, 1)
  122. assert.equal(manager.snapshot().phase, 'failed')
  123. })
  124. test('a native error during cleanup waits for cleanup before restoring', async t => {
  125. const pending = deferred(); let restored = 0
  126. const { manager, driver, calls } = fixture(t, { prepareInstall: () => pending.promise, recoverInstall: async () => { restored++ } })
  127. await manager.check(); await manager.download()
  128. const installing = manager.install()
  129. driver.emit('error', new Error('native staging lost'))
  130. assert.equal(restored, 0)
  131. pending.resolve(); await installing
  132. assert.equal(restored, 1); assert.deepEqual(calls, [])
  133. })
  134. test('quit dispatch failure restores app access and keeps installation locked', async t => {
  135. let restored = 0
  136. const { manager, driver } = fixture(t, { recoverInstall: async () => { restored++ } })
  137. driver.quitAndInstall = () => { throw new Error('native restart failed') }
  138. await manager.check(); await manager.download(); await manager.install()
  139. assert.equal(restored, 1); assert.equal(manager.snapshot().retryable, false)
  140. })
  141. test('automatic checks start once and stop cleanly', async t => {
  142. t.mock.timers.enable({ apis: ['setTimeout', 'setInterval'] })
  143. const { manager, driver } = fixture(t); let checks = 0
  144. driver.checkForUpdates = async () => { checks++; return { isUpdateAvailable: false, updateInfo: { version: '0.4.62' } } }
  145. manager.startChecking(); manager.startChecking()
  146. t.mock.timers.tick(15000); await Promise.resolve(); assert.equal(checks, 1)
  147. t.mock.timers.tick(3600000); await Promise.resolve(); assert.equal(checks, 2)
  148. manager.stopChecking(); t.mock.timers.tick(3600000); assert.equal(checks, 2)
  149. })