Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 108 additions & 3 deletions src/updater.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,70 @@
import { describe, it, expect } from 'vitest'

import { compareVersions, shouldUpdate } from './updater'
import { execFileSync } from 'node:child_process'
import { copyFileSync, mkdtempSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'

import { beforeEach, describe, it, expect, type Mock, vi } from 'vitest'

vi.mock('node:child_process', () => ({
execFileSync: vi.fn(),
}))

vi.mock('node:fs', () => ({
chmodSync: vi.fn(),
copyFileSync: vi.fn(),
mkdtempSync: vi.fn(),
renameSync: vi.fn(),
rmSync: vi.fn(),
unlinkSync: vi.fn(),
writeFileSync: vi.fn(),
}))

vi.mock('./logger.js', () => ({
log: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
}))

vi.mock('./platform.js', () => ({
getBinaryInstallPath: vi.fn(() => 'C:\\Program Files\\Glean\\glean-mdm.exe'),
getPlatform: vi.fn(() => 'win32'),
getTargetName: vi.fn(() => 'windows-x64'),
}))

import { checkForUpdate, compareVersions, shouldUpdate } from './updater'

const mockCopyFileSync = copyFileSync as Mock
const mockExecFileSync = execFileSync as Mock
const mockMkdtempSync = mkdtempSync as Mock
const mockRenameSync = renameSync as Mock
const mockRmSync = rmSync as Mock
const mockUnlinkSync = unlinkSync as Mock
const mockWriteFileSync = writeFileSync as Mock

function okJson(body: unknown) {
return {
ok: true,
status: 200,
json: vi.fn().mockResolvedValue(body),
text: vi.fn().mockResolvedValue(JSON.stringify(body)),
}
}

function okBinary(bytes = new Uint8Array([1, 2, 3])) {
return {
ok: true,
status: 200,
arrayBuffer: vi.fn().mockResolvedValue(bytes.buffer),
text: vi.fn().mockResolvedValue(''),
}
}

beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('fetch', vi.fn())
mockMkdtempSync.mockReturnValue('/tmp/glean-mdm-update-test')
mockWriteFileSync.mockReturnValue(undefined)
mockRmSync.mockReturnValue(undefined)
mockUnlinkSync.mockReturnValue(undefined)
mockCopyFileSync.mockReturnValue(undefined)
mockRenameSync.mockReturnValue(undefined)
})

describe('compareVersions', () => {
it('returns 0 for equal versions', () => {
Expand Down Expand Up @@ -76,3 +140,44 @@ describe('shouldUpdate', () => {
expect(shouldUpdate('1.0.0', '3.0.0', '1.2.3')).toBe(true)
})
})

describe('checkForUpdate error behavior', () => {
it('throws when version metadata omits version', async () => {
;(fetch as Mock).mockResolvedValueOnce(okJson({}))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1/5 (nit, non-blocking)

what's with the leading ; in these test bodies?


await expect(checkForUpdate('https://example.com/version.json', 'https://example.com/bin')).rejects.toThrow()
})

it('returns false and does not re-exec when replacing a renamed Windows binary fails', async () => {
;(fetch as Mock).mockResolvedValueOnce(okJson({ version: '9.9.9' })).mockResolvedValueOnce(okBinary())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1/5 (nit, non-blocking)

we have a whole block of mocks at the top of this file to avoid all this typecasting in test bodies. Consider adding a mockFetch there

mockRenameSync
.mockReturnValueOnce(undefined)
.mockImplementationOnce(() => {
throw new Error('replace failed')
})

await expect(checkForUpdate('https://example.com/version.json', 'https://example.com/bin')).resolves.toBe(false)

expect(mockExecFileSync).not.toHaveBeenCalled()
expect(mockRmSync).toHaveBeenCalledWith('/tmp/glean-mdm-update-test', { force: true, recursive: true })
})

it('returns false and does not re-exec when locked Windows pending write fails', async () => {
;(fetch as Mock).mockResolvedValueOnce(okJson({ version: '9.9.9' })).mockResolvedValueOnce(okBinary())
mockRenameSync
.mockImplementationOnce(() => {
throw new Error('binary locked')
})
.mockImplementationOnce(() => {
throw new Error('pending rename failed')
})
mockCopyFileSync.mockImplementationOnce(() => {
throw new Error('copy failed')
})

await expect(checkForUpdate('https://example.com/version.json', 'https://example.com/bin')).resolves.toBe(false)

expect(mockExecFileSync).not.toHaveBeenCalled()
expect(mockRmSync).toHaveBeenCalledWith('/tmp/glean-mdm-update-test', { force: true, recursive: true })
})
})