test: cover electron boundary modules
This commit is contained in:
86
tests/unit/auth-handler.test.ts
Normal file
86
tests/unit/auth-handler.test.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { IPC_CHANNELS } from '../../src/shared/ipc-channels'
|
||||||
|
|
||||||
|
const handleMock = vi.fn()
|
||||||
|
const withErrorHandlingMock = vi.fn(async (handler: () => Promise<unknown>) => {
|
||||||
|
const data = await handler()
|
||||||
|
return { success: true, data }
|
||||||
|
})
|
||||||
|
|
||||||
|
const serviceMethods = {
|
||||||
|
getComputerName: vi.fn(async () => 'TEST-PC'),
|
||||||
|
silentLogin: vi.fn(async () => ({ success: true })),
|
||||||
|
login: vi.fn(async (username: string) => ({ success: true, username })),
|
||||||
|
logout: vi.fn(async () => undefined),
|
||||||
|
getCurrentUser: vi.fn(async () => ({ success: true })),
|
||||||
|
getAllUsers: vi.fn(async () => []),
|
||||||
|
switchUser: vi.fn(async (user: unknown) => ({ success: true, user })),
|
||||||
|
isAdmin: vi.fn(async () => true)
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcMain: {
|
||||||
|
handle: handleMock
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/ipc/index', () => ({
|
||||||
|
withErrorHandling: withErrorHandlingMock
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/auth/auth-application-service', () => ({
|
||||||
|
AuthApplicationService: class {
|
||||||
|
getComputerName = serviceMethods.getComputerName
|
||||||
|
silentLogin = serviceMethods.silentLogin
|
||||||
|
login = serviceMethods.login
|
||||||
|
logout = serviceMethods.logout
|
||||||
|
getCurrentUser = serviceMethods.getCurrentUser
|
||||||
|
getAllUsers = serviceMethods.getAllUsers
|
||||||
|
switchUser = serviceMethods.switchUser
|
||||||
|
isAdmin = serviceMethods.isAdmin
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('registerAuthHandlers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('registers auth IPC handlers and delegates login requests', async () => {
|
||||||
|
const { registerAuthHandlers } = await import('../../src/main/ipc/auth-handler')
|
||||||
|
|
||||||
|
registerAuthHandlers()
|
||||||
|
|
||||||
|
expect(handleMock).toHaveBeenCalledWith(IPC_CHANNELS.AUTH_LOGIN, expect.any(Function))
|
||||||
|
expect(handleMock).toHaveBeenCalledWith(IPC_CHANNELS.AUTH_SILENT_LOGIN, expect.any(Function))
|
||||||
|
expect(handleMock).toHaveBeenCalledTimes(8)
|
||||||
|
|
||||||
|
const loginHandler = handleMock.mock.calls.find(
|
||||||
|
([channel]) => channel === IPC_CHANNELS.AUTH_LOGIN
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
const result = await loginHandler?.({}, { username: 'alice', password: 'secret' })
|
||||||
|
|
||||||
|
expect(serviceMethods.login).toHaveBeenCalledWith('alice', 'secret')
|
||||||
|
expect(withErrorHandlingMock).toHaveBeenCalled()
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { success: true, username: 'alice' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delegates switch user requests through the service layer', async () => {
|
||||||
|
const { registerAuthHandlers } = await import('../../src/main/ipc/auth-handler')
|
||||||
|
const userInfo = { id: 1, username: 'admin' }
|
||||||
|
|
||||||
|
registerAuthHandlers()
|
||||||
|
|
||||||
|
const switchUserHandler = handleMock.mock.calls.find(
|
||||||
|
([channel]) => channel === IPC_CHANNELS.AUTH_SWITCH_USER
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
await switchUserHandler?.({}, userInfo)
|
||||||
|
|
||||||
|
expect(serviceMethods.switchUser).toHaveBeenCalledWith(userInfo)
|
||||||
|
})
|
||||||
|
})
|
||||||
95
tests/unit/bootstrap-runtime.test.ts
Normal file
95
tests/unit/bootstrap-runtime.test.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
const mkdirSyncMock = vi.fn()
|
||||||
|
const existsSyncMock = vi.fn()
|
||||||
|
const readdirSyncMock = vi.fn()
|
||||||
|
const showErrorBoxMock = vi.fn()
|
||||||
|
const setAppUserModelIdMock = vi.fn()
|
||||||
|
const appOnMock = vi.fn()
|
||||||
|
const configInitializeMock = vi.fn(async () => undefined)
|
||||||
|
const updateInitializeMock = vi.fn()
|
||||||
|
const registerIpcHandlersMock = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('fs', () => ({
|
||||||
|
default: {
|
||||||
|
mkdirSync: mkdirSyncMock,
|
||||||
|
existsSync: existsSyncMock,
|
||||||
|
readdirSync: readdirSyncMock
|
||||||
|
},
|
||||||
|
mkdirSync: mkdirSyncMock,
|
||||||
|
existsSync: existsSyncMock,
|
||||||
|
readdirSync: readdirSyncMock
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
getPath: vi.fn(() => 'D:/userData'),
|
||||||
|
setAppUserModelId: setAppUserModelIdMock,
|
||||||
|
on: appOnMock,
|
||||||
|
isPackaged: false
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
showErrorBox: showErrorBoxMock
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/config/config-manager', () => ({
|
||||||
|
ConfigManager: {
|
||||||
|
getInstance: vi.fn(() => ({
|
||||||
|
initialize: configInitializeMock
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/update/update-service', () => ({
|
||||||
|
UpdateService: {
|
||||||
|
getInstance: vi.fn(() => ({
|
||||||
|
initialize: updateInitializeMock
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/ipc', () => ({
|
||||||
|
registerIpcHandlers: registerIpcHandlersMock
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('bootstrap runtime', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
existsSyncMock.mockReset()
|
||||||
|
readdirSyncMock.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('configures Playwright browser path under app userData', async () => {
|
||||||
|
const { configurePlaywrightBrowsersPath } = await import('../../src/main/bootstrap/runtime')
|
||||||
|
|
||||||
|
const result = configurePlaywrightBrowsersPath()
|
||||||
|
|
||||||
|
const expectedPath = join('D:/userData', 'ms-playwright')
|
||||||
|
expect(result).toBe(expectedPath)
|
||||||
|
expect(process.env.PLAYWRIGHT_BROWSERS_PATH).toBe(expectedPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes config, update service and IPC registration', async () => {
|
||||||
|
const { initializeMainProcessServices } = await import('../../src/main/bootstrap/runtime')
|
||||||
|
|
||||||
|
await initializeMainProcessServices()
|
||||||
|
|
||||||
|
expect(configInitializeMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(updateInitializeMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(registerIpcHandlersMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows an error dialog when no Playwright browser is found', async () => {
|
||||||
|
const { ensurePlaywrightRuntime } = await import('../../src/main/bootstrap/runtime')
|
||||||
|
|
||||||
|
existsSyncMock.mockReturnValue(false)
|
||||||
|
readdirSyncMock.mockReturnValue([])
|
||||||
|
|
||||||
|
ensurePlaywrightRuntime('D:/userData/ms-playwright')
|
||||||
|
|
||||||
|
expect(mkdirSyncMock).toHaveBeenCalledWith('D:/userData/ms-playwright', { recursive: true })
|
||||||
|
expect(showErrorBoxMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
75
tests/unit/cleaner-handler.test.ts
Normal file
75
tests/unit/cleaner-handler.test.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { IPC_CHANNELS } from '../../src/shared/ipc-channels'
|
||||||
|
|
||||||
|
const handleMock = vi.fn()
|
||||||
|
const withErrorHandlingMock = vi.fn(async (handler: () => Promise<unknown>) => {
|
||||||
|
const data = await handler()
|
||||||
|
return { success: true, data }
|
||||||
|
})
|
||||||
|
|
||||||
|
const serviceMethods = {
|
||||||
|
runCleaner: vi.fn(async (_sender: unknown, input: unknown) => ({ success: true, input })),
|
||||||
|
exportResults: vi.fn(async (items: unknown[]) => ({ success: true, total: items.length }))
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
ipcMain: {
|
||||||
|
handle: handleMock
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/ipc/index', () => ({
|
||||||
|
withErrorHandling: withErrorHandlingMock
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/cleaner/cleaner-application-service', () => ({
|
||||||
|
CleanerApplicationService: class {
|
||||||
|
runCleaner = serviceMethods.runCleaner
|
||||||
|
exportResults = serviceMethods.exportResults
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('registerCleanerHandlers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('registers cleaner IPC handlers and forwards the sender to the application service', async () => {
|
||||||
|
const { registerCleanerHandlers } = await import('../../src/main/ipc/cleaner-handler')
|
||||||
|
const sender = { id: 99 }
|
||||||
|
const event = { sender }
|
||||||
|
const input = { orderNumbers: ['MO-001'] }
|
||||||
|
|
||||||
|
registerCleanerHandlers()
|
||||||
|
|
||||||
|
expect(handleMock).toHaveBeenCalledTimes(2)
|
||||||
|
expect(handleMock).toHaveBeenCalledWith(IPC_CHANNELS.CLEANER_RUN, expect.any(Function))
|
||||||
|
|
||||||
|
const runHandler = handleMock.mock.calls.find(
|
||||||
|
([channel]) => channel === IPC_CHANNELS.CLEANER_RUN
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
const result = await runHandler?.(event, input)
|
||||||
|
|
||||||
|
expect(serviceMethods.runCleaner).toHaveBeenCalledWith(sender, input)
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { success: true, input }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forwards export requests to the cleaner application service', async () => {
|
||||||
|
const { registerCleanerHandlers } = await import('../../src/main/ipc/cleaner-handler')
|
||||||
|
const items = [{ id: '1' }, { id: '2' }]
|
||||||
|
|
||||||
|
registerCleanerHandlers()
|
||||||
|
|
||||||
|
const exportHandler = handleMock.mock.calls.find(
|
||||||
|
([channel]) => channel === IPC_CHANNELS.CLEANER_EXPORT_RESULTS
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
await exportHandler?.({}, items)
|
||||||
|
|
||||||
|
expect(serviceMethods.exportResults).toHaveBeenCalledWith(items)
|
||||||
|
})
|
||||||
|
})
|
||||||
136
tests/unit/update-catalog-service.test.ts
Normal file
136
tests/unit/update-catalog-service.test.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { UpdateCatalogService } from '../../src/main/services/update/update-catalog-service'
|
||||||
|
import type { UpdateConfig } from '../../src/main/types/config.schema'
|
||||||
|
import type { UpdateCatalog, UpdateRelease, UpdateStatus } from '../../src/main/types/update.types'
|
||||||
|
|
||||||
|
const config: UpdateConfig = {
|
||||||
|
enabled: true,
|
||||||
|
allowDevMode: false,
|
||||||
|
endpoint: 'http://localhost:9000',
|
||||||
|
accessKey: 'key',
|
||||||
|
secretKey: 'secret',
|
||||||
|
bucket: 'bucket',
|
||||||
|
region: 'us-east-1',
|
||||||
|
basePrefix: 'updates/win-portable',
|
||||||
|
checkIntervalMinutes: 30,
|
||||||
|
maxAdminHistoryPerChannel: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRelease(
|
||||||
|
version: string,
|
||||||
|
channel: 'stable' | 'preview',
|
||||||
|
overrides: Partial<UpdateRelease> = {}
|
||||||
|
): UpdateRelease {
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
channel,
|
||||||
|
artifactKey: `${channel}/${version}.exe`,
|
||||||
|
sha256: `${channel}-${version}-sha`,
|
||||||
|
size: 1,
|
||||||
|
publishedAt: '2026-03-21T10:00:00Z',
|
||||||
|
changelogKey: `${channel}/${version}.md`,
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStatus(overrides: Partial<UpdateStatus> = {}): UpdateStatus {
|
||||||
|
return {
|
||||||
|
enabled: true,
|
||||||
|
supported: true,
|
||||||
|
phase: 'idle',
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
currentChannel: 'stable',
|
||||||
|
currentUserType: 'User',
|
||||||
|
...overrides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('UpdateCatalogService', () => {
|
||||||
|
it('returns disabled dialog catalog when updates are unavailable', () => {
|
||||||
|
const storageClient = { readText: vi.fn() }
|
||||||
|
const installer = { getValidDownloadedRelease: vi.fn() }
|
||||||
|
const service = new UpdateCatalogService(config, storageClient as never, installer as never)
|
||||||
|
|
||||||
|
const result = service.getDialogCatalog(
|
||||||
|
createStatus({ enabled: false, currentUserType: null }),
|
||||||
|
{ stable: [], preview: [] }
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual({ mode: 'disabled' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('loads stable and preview catalogs for admin users', async () => {
|
||||||
|
const storageClient = {
|
||||||
|
readText: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(JSON.stringify({ releases: [createRelease('1.1.0', 'stable')] }))
|
||||||
|
.mockResolvedValueOnce(JSON.stringify({ releases: [createRelease('1.2.0', 'preview')] }))
|
||||||
|
}
|
||||||
|
const installer = { getValidDownloadedRelease: vi.fn() }
|
||||||
|
const service = new UpdateCatalogService(config, storageClient as never, installer as never)
|
||||||
|
|
||||||
|
const result = await service.loadCatalog('Admin')
|
||||||
|
|
||||||
|
expect(storageClient.readText).toHaveBeenCalledTimes(2)
|
||||||
|
expect(result.stable[0]?.version).toBe('1.1.0')
|
||||||
|
expect(result.preview[0]?.version).toBe('1.2.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks user update as downloaded when a verified package already exists', async () => {
|
||||||
|
const recommended = createRelease('1.1.0', 'stable')
|
||||||
|
const storageClient = { readText: vi.fn() }
|
||||||
|
const installer = {
|
||||||
|
getValidDownloadedRelease: vi.fn().mockResolvedValue({
|
||||||
|
...recommended,
|
||||||
|
localPath: 'D:/downloads/stable-1.1.0.exe'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const service = new UpdateCatalogService(config, storageClient as never, installer as never)
|
||||||
|
|
||||||
|
const result = await service.resolveUserStatus(createStatus(), {
|
||||||
|
stable: [recommended],
|
||||||
|
preview: []
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.phase).toBe('downloaded')
|
||||||
|
expect(result.downloadedRelease?.localPath).toContain('stable-1.1.0.exe')
|
||||||
|
expect(result.recommendedRelease?.version).toBe('1.1.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns available admin status and trims catalog history', () => {
|
||||||
|
const storageClient = { readText: vi.fn() }
|
||||||
|
const installer = { getValidDownloadedRelease: vi.fn() }
|
||||||
|
const service = new UpdateCatalogService(config, storageClient as never, installer as never)
|
||||||
|
const catalog: UpdateCatalog = {
|
||||||
|
stable: [
|
||||||
|
createRelease('1.0.0', 'stable'),
|
||||||
|
createRelease('1.1.0', 'stable'),
|
||||||
|
createRelease('1.2.0', 'stable')
|
||||||
|
],
|
||||||
|
preview: [
|
||||||
|
createRelease('1.3.0', 'preview'),
|
||||||
|
createRelease('1.4.0', 'preview'),
|
||||||
|
createRelease('1.5.0', 'preview')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminStatus = service.resolveAdminStatus(
|
||||||
|
createStatus({ currentUserType: 'Admin', currentVersion: '1.0.0' }),
|
||||||
|
catalog
|
||||||
|
)
|
||||||
|
const dialogCatalog = service.getDialogCatalog(
|
||||||
|
createStatus({
|
||||||
|
currentUserType: 'Admin',
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
recommendedRelease: createRelease('1.5.0', 'preview')
|
||||||
|
}),
|
||||||
|
catalog
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(adminStatus.phase).toBe('available')
|
||||||
|
expect(adminStatus.recommendedRelease?.version).toBe('1.5.0')
|
||||||
|
expect(dialogCatalog.mode).toBe('admin')
|
||||||
|
expect(dialogCatalog.channels?.stable).toHaveLength(2)
|
||||||
|
expect(dialogCatalog.channels?.preview).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
33
tests/unit/update-installer.test.ts
Normal file
33
tests/unit/update-installer.test.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { UpdateInstaller } from '../../src/main/services/update/update-installer'
|
||||||
|
|
||||||
|
describe('UpdateInstaller', () => {
|
||||||
|
it('builds downloaded package path under userData pending-update', () => {
|
||||||
|
const installer = new UpdateInstaller()
|
||||||
|
|
||||||
|
const result = installer.getDownloadPath({
|
||||||
|
version: '1.2.3',
|
||||||
|
channel: 'stable'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toContain(path.join('logs', 'pending-update'))
|
||||||
|
expect(result).toContain('stable-1.2.3.exe')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns null when downloaded package does not exist', async () => {
|
||||||
|
const installer = new UpdateInstaller()
|
||||||
|
|
||||||
|
const result = await installer.getValidDownloadedRelease({
|
||||||
|
version: '9.9.9',
|
||||||
|
channel: 'preview',
|
||||||
|
artifactKey: 'preview/9.9.9.exe',
|
||||||
|
sha256: 'missing',
|
||||||
|
size: 1,
|
||||||
|
publishedAt: '2026-03-21T10:00:00Z',
|
||||||
|
changelogKey: 'preview/9.9.9.md'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
196
tests/unit/update-service.test.ts
Normal file
196
tests/unit/update-service.test.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import type { UpdateConfig } from '../../src/main/types/config.schema'
|
||||||
|
import type { UpdateCatalog, UpdateRelease, UpdateStatus } from '../../src/main/types/update.types'
|
||||||
|
|
||||||
|
const mockPublishUpdateStatus = vi.fn()
|
||||||
|
const mockReadText = vi.fn()
|
||||||
|
const mockDownloadToFile = vi.fn()
|
||||||
|
const mockLoadCatalog = vi.fn()
|
||||||
|
const mockGetDialogCatalog = vi.fn()
|
||||||
|
const mockResolveUserStatus = vi.fn()
|
||||||
|
const mockResolveAdminStatus = vi.fn()
|
||||||
|
const mockGetDownloadPath = vi.fn()
|
||||||
|
const mockCalculateSha256 = vi.fn()
|
||||||
|
const mockInstallDownloadedRelease = vi.fn()
|
||||||
|
const mockGetValidDownloadedRelease = vi.fn()
|
||||||
|
|
||||||
|
let currentUpdateConfig: UpdateConfig = {
|
||||||
|
enabled: true,
|
||||||
|
allowDevMode: true,
|
||||||
|
endpoint: 'http://localhost:9000',
|
||||||
|
accessKey: 'key',
|
||||||
|
secretKey: 'secret',
|
||||||
|
bucket: 'bucket',
|
||||||
|
region: 'us-east-1',
|
||||||
|
basePrefix: 'updates/win-portable',
|
||||||
|
checkIntervalMinutes: 30,
|
||||||
|
maxAdminHistoryPerChannel: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRelease(version: string, channel: 'stable' | 'preview' = 'stable'): UpdateRelease {
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
channel,
|
||||||
|
artifactKey: `${channel}/${version}.exe`,
|
||||||
|
sha256: `${channel}-${version}-sha`,
|
||||||
|
size: 1,
|
||||||
|
publishedAt: '2026-03-21T10:00:00Z',
|
||||||
|
changelogKey: `${channel}/${version}.md`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/config/config-manager', () => ({
|
||||||
|
ConfigManager: {
|
||||||
|
getInstance: vi.fn(() => ({
|
||||||
|
getConfig: () => ({ update: currentUpdateConfig })
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/logger', () => ({
|
||||||
|
createLogger: vi.fn(() => ({
|
||||||
|
info: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
debug: vi.fn()
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/update/update-status-publisher', () => ({
|
||||||
|
publishUpdateStatus: mockPublishUpdateStatus
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/update/update-storage-client', () => ({
|
||||||
|
UpdateStorageClient: class {
|
||||||
|
readText = mockReadText
|
||||||
|
downloadToFile = mockDownloadToFile
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/update/update-catalog-service', () => ({
|
||||||
|
UpdateCatalogService: class {
|
||||||
|
loadCatalog = mockLoadCatalog
|
||||||
|
getDialogCatalog = mockGetDialogCatalog
|
||||||
|
resolveUserStatus = mockResolveUserStatus
|
||||||
|
resolveAdminStatus = mockResolveAdminStatus
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../../src/main/services/update/update-installer', () => ({
|
||||||
|
UpdateInstaller: class {
|
||||||
|
getDownloadPath = mockGetDownloadPath
|
||||||
|
calculateSha256 = mockCalculateSha256
|
||||||
|
installDownloadedRelease = mockInstallDownloadedRelease
|
||||||
|
getValidDownloadedRelease = mockGetValidDownloadedRelease
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('UpdateService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
currentUpdateConfig = {
|
||||||
|
enabled: true,
|
||||||
|
allowDevMode: true,
|
||||||
|
endpoint: 'http://localhost:9000',
|
||||||
|
accessKey: 'key',
|
||||||
|
secretKey: 'secret',
|
||||||
|
bucket: 'bucket',
|
||||||
|
region: 'us-east-1',
|
||||||
|
basePrefix: 'updates/win-portable',
|
||||||
|
checkIntervalMinutes: 30,
|
||||||
|
maxAdminHistoryPerChannel: 10
|
||||||
|
}
|
||||||
|
mockReadText.mockReset()
|
||||||
|
mockDownloadToFile.mockReset()
|
||||||
|
mockLoadCatalog.mockReset()
|
||||||
|
mockGetDialogCatalog.mockReset()
|
||||||
|
mockResolveUserStatus.mockReset()
|
||||||
|
mockResolveAdminStatus.mockReset()
|
||||||
|
mockGetDownloadPath.mockReset()
|
||||||
|
mockCalculateSha256.mockReset()
|
||||||
|
mockInstallDownloadedRelease.mockReset()
|
||||||
|
mockGetValidDownloadedRelease.mockReset()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadService() {
|
||||||
|
vi.resetModules()
|
||||||
|
const module = await import('../../src/main/services/update/update-service')
|
||||||
|
;(module.UpdateService as unknown as { instance: unknown }).instance = null
|
||||||
|
return module.UpdateService.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
it('initializes enabled update status from config', async () => {
|
||||||
|
const service = await loadService()
|
||||||
|
|
||||||
|
service.initialize()
|
||||||
|
|
||||||
|
expect(service.getStatus()).toMatchObject({
|
||||||
|
enabled: true,
|
||||||
|
supported: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears update state when user context becomes guest-like', async () => {
|
||||||
|
const service = await loadService()
|
||||||
|
|
||||||
|
await service.setUserContext(null)
|
||||||
|
|
||||||
|
expect(service.getStatus()).toMatchObject({
|
||||||
|
currentUserType: null,
|
||||||
|
phase: 'idle',
|
||||||
|
recommendedRelease: undefined,
|
||||||
|
downloadedRelease: undefined
|
||||||
|
})
|
||||||
|
expect(mockPublishUpdateStatus).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('checks updates for user and auto-downloads available recommendation', async () => {
|
||||||
|
const recommended = createRelease('1.1.0')
|
||||||
|
const catalog: UpdateCatalog = {
|
||||||
|
stable: [recommended],
|
||||||
|
preview: []
|
||||||
|
}
|
||||||
|
const userStatus: Partial<UpdateStatus> = {
|
||||||
|
phase: 'available',
|
||||||
|
recommendedRelease: recommended,
|
||||||
|
latestVersion: recommended.version,
|
||||||
|
latestChannel: recommended.channel,
|
||||||
|
message: `发现稳定版 ${recommended.version}`
|
||||||
|
}
|
||||||
|
|
||||||
|
mockLoadCatalog.mockResolvedValue(catalog)
|
||||||
|
mockResolveUserStatus.mockResolvedValue(userStatus)
|
||||||
|
mockGetDownloadPath.mockReturnValue('D:/downloads/stable-1.1.0.exe')
|
||||||
|
mockCalculateSha256.mockResolvedValue(recommended.sha256)
|
||||||
|
|
||||||
|
const service = await loadService()
|
||||||
|
await service.setUserContext('User')
|
||||||
|
|
||||||
|
expect(mockLoadCatalog).toHaveBeenCalledWith('User')
|
||||||
|
expect(mockResolveUserStatus).toHaveBeenCalled()
|
||||||
|
expect(mockDownloadToFile).toHaveBeenCalledWith(
|
||||||
|
recommended.artifactKey,
|
||||||
|
'D:/downloads/stable-1.1.0.exe'
|
||||||
|
)
|
||||||
|
expect(service.getStatus()).toMatchObject({
|
||||||
|
phase: 'downloaded',
|
||||||
|
latestVersion: '1.1.0',
|
||||||
|
latestChannel: 'stable'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns disabled catalog when update services are unavailable', async () => {
|
||||||
|
currentUpdateConfig = {
|
||||||
|
...currentUpdateConfig,
|
||||||
|
enabled: false,
|
||||||
|
allowDevMode: false
|
||||||
|
}
|
||||||
|
const service = await loadService()
|
||||||
|
service.initialize()
|
||||||
|
|
||||||
|
const result = service.getCatalog()
|
||||||
|
|
||||||
|
expect(result).toEqual({ mode: 'disabled' })
|
||||||
|
expect(mockGetDialogCatalog).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
36
tests/unit/update-status-publisher.test.ts
Normal file
36
tests/unit/update-status-publisher.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const mockSend = vi.fn()
|
||||||
|
const mockGetAllWindows = vi.fn(() => [
|
||||||
|
{ webContents: { send: mockSend } },
|
||||||
|
{ webContents: { send: mockSend } }
|
||||||
|
])
|
||||||
|
|
||||||
|
vi.mock('electron', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('electron')>('electron')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
BrowserWindow: {
|
||||||
|
getAllWindows: mockGetAllWindows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('update-status-publisher', () => {
|
||||||
|
it('broadcasts status to all renderer windows', async () => {
|
||||||
|
const { publishUpdateStatus } =
|
||||||
|
await import('../../src/main/services/update/update-status-publisher')
|
||||||
|
|
||||||
|
publishUpdateStatus({
|
||||||
|
enabled: true,
|
||||||
|
supported: true,
|
||||||
|
phase: 'checking',
|
||||||
|
currentVersion: '1.0.0',
|
||||||
|
currentChannel: 'stable',
|
||||||
|
currentUserType: 'Admin'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockGetAllWindows).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user