update-http-executor.spec.ts 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. import { EventEmitter } from 'node:events'
  2. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  3. import type { ClientRequest } from 'electron'
  4. import { DesktopUpdateHttpExecutor } from '../src/update-http-executor.ts'
  5. beforeEach(() => { vi.useFakeTimers() })
  6. afterEach(() => { vi.useRealTimers() })
  7. function fixture() {
  8. const request = Object.assign(new EventEmitter(), { abort: vi.fn(() => { request.emit('abort') }) })
  9. const reject = vi.fn()
  10. new DesktopUpdateHttpExecutor(1000).addErrorAndTimeoutHandlers(request as unknown as ClientRequest, reject)
  11. return { request, reject }
  12. }
  13. describe('Electron updater inactivity deadline', () => {
  14. it('aborts a request that never receives headers without waiting for a Node socket event', async () => {
  15. const { request, reject } = fixture()
  16. await vi.advanceTimersByTimeAsync(1000)
  17. expect(reject).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ code: 'ETIMEDOUT' }))
  18. expect(request.abort).toHaveBeenCalledOnce()
  19. expect(vi.getTimerCount()).toBe(0)
  20. })
  21. it('refreshes the deadline on actual response bytes rather than timing out a long active download', async () => {
  22. const { request, reject } = fixture()
  23. const response = new EventEmitter()
  24. request.emit('response', response)
  25. for (let index = 0; index < 4; index++) {
  26. await vi.advanceTimersByTimeAsync(900)
  27. response.emit('data', Buffer.from('data'))
  28. }
  29. expect(reject).not.toHaveBeenCalled()
  30. await vi.advanceTimersByTimeAsync(1000)
  31. expect(reject).toHaveBeenCalledOnce()
  32. expect(response.listenerCount('data')).toBe(0)
  33. })
  34. it.each(['abort', 'error'])('removes the deadline on request %s', async (event) => {
  35. const { request, reject } = fixture()
  36. request.emit(event, new Error('transport failure'))
  37. const count = reject.mock.calls.length
  38. await vi.advanceTimersByTimeAsync(10_000)
  39. expect(request.abort).not.toHaveBeenCalled()
  40. expect(reject).toHaveBeenCalledTimes(count)
  41. expect(vi.getTimerCount()).toBe(0)
  42. })
  43. it('keeps waiting for headers after Electron closes the request writable stream', async () => {
  44. const { request, reject } = fixture()
  45. request.emit('finish')
  46. request.emit('close')
  47. await vi.advanceTimersByTimeAsync(1000)
  48. expect(reject).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ code: 'ETIMEDOUT' }))
  49. })
  50. it('does not leave a deadline running while completed bytes undergo verification', async () => {
  51. const { request, reject } = fixture()
  52. const response = new EventEmitter()
  53. request.emit('response', response)
  54. response.emit('end')
  55. await vi.advanceTimersByTimeAsync(10_000)
  56. expect(reject).not.toHaveBeenCalled()
  57. expect(vi.getTimerCount()).toBe(0)
  58. })
  59. it.each([0, 999, 1.5, NaN, 2_147_483_648])('rejects invalid inactivity configuration: %s', (value) => {
  60. expect(() => new DesktopUpdateHttpExecutor(value)).toThrow('idle timeout')
  61. })
  62. })