eigent/test/unit/electron/main/index.test.ts
Muhammet Eren Karakuş 413df36cd8
fix: patch 5 security vulnerabilities across electron, server, and proxy layers (#1292)
Co-authored-by: bytecii <994513625@qq.com>
2026-02-21 16:39:26 -08:00

1407 lines
44 KiB
TypeScript

// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock all modules first
const mockApp = {
getPath: vi.fn(),
getVersion: vi.fn(),
getLocale: vi.fn(),
requestSingleInstanceLock: vi.fn(),
quit: vi.fn(),
setAsDefaultProtocolClient: vi.fn(),
isDefaultProtocolClient: vi.fn(),
setAppUserModelId: vi.fn(),
disableHardwareAcceleration: vi.fn(),
commandLine: {
appendSwitch: vi.fn(),
},
whenReady: vi.fn(),
on: vi.fn(),
};
const mockBrowserWindow = vi.fn(() => ({
loadURL: vi.fn(),
loadFile: vi.fn(),
show: vi.fn(),
close: vi.fn(),
minimize: vi.fn(),
isMaximized: vi.fn(),
maximize: vi.fn(),
unmaximize: vi.fn(),
isDestroyed: vi.fn(),
isFullScreen: vi.fn(),
webContents: {
openDevTools: vi.fn(),
send: vi.fn(),
on: vi.fn(),
setWindowOpenHandler: vi.fn(),
toggleDevTools: vi.fn(),
},
getAllWindows: vi.fn(),
}));
const mockDialog = {
showOpenDialog: vi.fn(),
showSaveDialog: vi.fn(),
};
const mockShell = {
openExternal: vi.fn(),
showItemInFolder: vi.fn(),
};
const mockIpcMain = {
handle: vi.fn(),
on: vi.fn(),
};
const mockMenu = {
setApplicationMenu: vi.fn(),
};
const mockFs = {
existsSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
createReadStream: vi.fn(),
unlinkSync: vi.fn(),
createWriteStream: vi.fn(),
};
const mockFsp = {
access: vi.fn(),
stat: vi.fn(),
readFile: vi.fn(),
writeFile: vi.fn(),
rm: vi.fn(),
};
const mockOs = {
release: vi.fn(),
homedir: vi.fn(),
};
const mockLog = {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
transports: {
file: {
getFile: vi.fn(() => ({ path: '/mock/log/path' })),
},
console: { level: 'info' },
},
};
const mockAxios = {
post: vi.fn(),
};
// Apply mocks
vi.mock('electron', () => ({
app: mockApp,
BrowserWindow: mockBrowserWindow,
ipcMain: mockIpcMain,
dialog: mockDialog,
shell: mockShell,
nativeTheme: { themeSource: '' },
protocol: { handle: vi.fn() },
session: { defaultSession: { on: vi.fn() } },
Menu: mockMenu,
}));
vi.mock('node:fs', () => ({
default: mockFs,
existsSync: mockFs.existsSync,
readFileSync: mockFs.readFileSync,
writeFileSync: mockFs.writeFileSync,
unlinkSync: mockFs.unlinkSync,
createReadStream: mockFs.createReadStream,
createWriteStream: mockFs.createWriteStream,
}));
vi.mock('fs/promises', () => mockFsp);
vi.mock('node:os', () => ({
default: mockOs,
homedir: mockOs.homedir,
release: mockOs.release,
}));
vi.mock('electron-log', () => ({ default: mockLog }));
vi.mock('axios', () => ({ default: mockAxios }));
vi.mock('form-data', () => ({
default: vi.fn(),
}));
// Mock internal modules (these can fail silently since we're testing logic, not actual imports)
vi.mock('../../../../electron/main/update', () => ({
update: vi.fn(),
registerUpdateIpcHandlers: vi.fn(),
}));
vi.mock('../../../../electron/main/init', () => ({
checkToolInstalled: vi.fn(),
installDependencies: vi.fn(),
killProcessOnPort: vi.fn(),
startBackend: vi.fn(),
findAvailablePort: vi.fn(),
}));
// Other internal mocks...
vi.mock('../../../../electron/main/webview', () => ({
WebViewManager: vi.fn(),
}));
vi.mock('../../../../electron/main/fileReader', () => ({
FileReader: vi.fn(),
}));
vi.mock('../../../../electron/main/utils/mcpConfig', () => ({
addMcp: vi.fn(),
removeMcp: vi.fn(),
updateMcp: vi.fn(),
readMcpConfig: vi.fn(),
}));
vi.mock('../../../../electron/main/utils/envUtil', () => ({
getEnvPath: vi.fn(),
updateEnvBlock: vi.fn(),
removeEnvKey: vi.fn(),
getEmailFolderPath: vi.fn(),
}));
vi.mock('../../../../electron/main/copy', () => ({ copyBrowserData: vi.fn() }));
vi.mock('../../../../electron/main/utils/log', () => ({ zipFolder: vi.fn() }));
vi.mock('tree-kill', () => ({ default: vi.fn() }));
// Import the mocked functions
import * as initModule from '../../../../electron/main/init';
import * as envUtil from '../../../../electron/main/utils/envUtil';
import * as mcpConfig from '../../../../electron/main/utils/mcpConfig';
// Cast the imports to mocked versions
const mockedEnvUtil = vi.mocked(envUtil);
const mockedMcpConfig = vi.mocked(mcpConfig);
const mockedInitModule = vi.mocked(initModule);
describe('Electron Main Index Functions', () => {
beforeEach(() => {
vi.clearAllMocks();
// Setup default mock returns
mockApp.getPath.mockReturnValue('/mock/user/data');
mockApp.getVersion.mockReturnValue('1.0.0');
mockApp.getLocale.mockReturnValue('en-US');
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue('1.0.0');
mockOs.release.mockReturnValue('10.0.0');
mockOs.homedir.mockReturnValue('/home/user');
});
afterEach(() => {
vi.clearAllMocks();
});
describe('checkAndInstallDepsOnUpdate', () => {
it('should return true when version file exists and matches current version', async () => {
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue('1.0.0');
mockApp.getVersion.mockReturnValue('1.0.0');
// We test the logic that would be in the function
const versionExists = mockFs.existsSync('/mock/version.txt');
const savedVersion = mockFs.readFileSync('/mock/version.txt', 'utf-8');
const currentVersion = mockApp.getVersion();
expect(versionExists).toBe(true);
expect(savedVersion).toBe(currentVersion);
});
it('should install dependencies when version file does not exist', async () => {
mockFs.existsSync.mockReturnValue(false);
mockApp.getVersion.mockReturnValue('1.0.0');
const versionExists = mockFs.existsSync('/mock/version.txt');
expect(versionExists).toBe(false);
});
it('should install dependencies when version has changed', async () => {
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue('0.9.0');
mockApp.getVersion.mockReturnValue('1.0.0');
const savedVersion = mockFs.readFileSync('/mock/version.txt', 'utf-8');
const currentVersion = mockApp.getVersion();
expect(savedVersion).not.toBe(currentVersion);
});
it('should handle errors during version check', async () => {
mockFs.existsSync.mockImplementation(() => {
throw new Error('File system error');
});
expect(() => mockFs.existsSync('/path')).toThrow('File system error');
});
});
describe('setupProtocolHandlers', () => {
it('should set up protocol handlers in development mode', () => {
process.env.NODE_ENV = 'development';
mockApp.isDefaultProtocolClient.mockReturnValue(false);
// Test protocol handler setup logic
expect(mockApp.setAsDefaultProtocolClient).toBeDefined();
});
it('should set up protocol handlers in production mode', () => {
process.env.NODE_ENV = 'production';
expect(mockApp.setAsDefaultProtocolClient).toBeDefined();
});
});
describe('handleProtocolUrl', () => {
let _mockWin: any;
beforeEach(() => {
_mockWin = {
isDestroyed: vi.fn().mockReturnValue(false),
webContents: {
send: vi.fn(),
},
};
});
it('should handle OAuth protocol URLs correctly', () => {
// Since custom protocols might not work in test env, we test URL parsing directly
const urlStr = 'https://example.com/oauth?provider=google&code=123456';
const urlObj = new URL(urlStr);
expect(urlObj.pathname).toBe('/oauth');
expect(urlObj.searchParams.get('provider')).toBe('google');
expect(urlObj.searchParams.get('code')).toBe('123456');
});
it('should handle authorization code URLs', () => {
const urlStr = 'https://example.com/callback?code=abc123';
const urlObj = new URL(urlStr);
expect(urlObj.searchParams.get('code')).toBe('abc123');
});
it('should handle share token URLs', () => {
const urlStr = 'https://example.com/share?share_token=token123';
const urlObj = new URL(urlStr);
expect(urlObj.searchParams.get('share_token')).toBe('token123');
});
it('should handle missing window gracefully', () => {
// Test error handling when window is not available
expect(mockLog.error).toBeDefined();
});
});
describe('setupSingleInstanceLock', () => {
it('should quit app when single instance lock fails', () => {
mockApp.requestSingleInstanceLock.mockReturnValue(false);
const gotLock = mockApp.requestSingleInstanceLock();
expect(gotLock).toBe(false);
});
it('should set up event handlers when single instance lock succeeds', () => {
mockApp.requestSingleInstanceLock.mockReturnValue(true);
const gotLock = mockApp.requestSingleInstanceLock();
expect(gotLock).toBe(true);
expect(mockApp.on).toBeDefined();
});
});
describe('getSystemLanguage', () => {
it('should return zh-cn for Chinese locale', async () => {
mockApp.getLocale.mockReturnValue('zh-CN');
const getSystemLanguage = async () => {
const locale = mockApp.getLocale();
return locale === 'zh-CN' ? 'zh-cn' : 'en';
};
const result = await getSystemLanguage();
expect(result).toBe('zh-cn');
});
it('should return en for other locales', async () => {
mockApp.getLocale.mockReturnValue('en-US');
const getSystemLanguage = async () => {
const locale = mockApp.getLocale();
return locale === 'zh-CN' ? 'zh-cn' : 'en';
};
const result = await getSystemLanguage();
expect(result).toBe('en');
});
});
describe('checkManagerInstance', () => {
it('should return manager when it exists', () => {
const mockManager = { test: 'value' };
const checkManagerInstance = (manager: any, name: string) => {
if (!manager) {
throw new Error(`${name} not initialized`);
}
return manager;
};
const result = checkManagerInstance(mockManager, 'TestManager');
expect(result).toBe(mockManager);
});
it('should throw error when manager is null or undefined', () => {
const checkManagerInstance = (manager: any, name: string) => {
if (!manager) {
throw new Error(`${name} not initialized`);
}
return manager;
};
expect(() => checkManagerInstance(null, 'TestManager')).toThrow(
'TestManager not initialized'
);
expect(() => checkManagerInstance(undefined, 'TestManager')).toThrow(
'TestManager not initialized'
);
});
});
describe('getBackupLogPath', () => {
it('should return correct backup log path', () => {
mockApp.getPath.mockReturnValue('/mock/userdata');
const getBackupLogPath = () => {
const userDataPath = mockApp.getPath('userData');
return `${userDataPath}/logs/main.log`;
};
const result = getBackupLogPath();
expect(result).toContain('logs');
expect(result).toContain('main.log');
});
});
describe('IPC Handlers', () => {
describe('get-browser-port handler', () => {
it('should return browser port', () => {
const mockHandler = vi.fn().mockReturnValue(9222);
expect(typeof mockHandler()).toBe('number');
});
});
describe('get-app-version handler', () => {
it('should return app version', () => {
mockApp.getVersion.mockReturnValue('1.0.0');
const result = mockApp.getVersion();
expect(result).toBe('1.0.0');
});
});
describe('get-backend-port handler', () => {
it('should return backend port', () => {
const mockHandler = vi.fn().mockReturnValue(5001);
expect(typeof mockHandler()).toBe('number');
});
});
describe('get-home-dir handler', () => {
it('should return USERPROFILE on Windows', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'win32' });
process.env.USERPROFILE = 'C:\\Users\\TestUser';
const getHomeDir = () => {
const platform = process.platform;
return platform === 'win32'
? process.env.USERPROFILE
: process.env.HOME;
};
const result = getHomeDir();
expect(result).toBe('C:\\Users\\TestUser');
// Restore original platform
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should return HOME on non-Windows', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'darwin' });
process.env.HOME = '/home/testuser';
const getHomeDir = () => {
const platform = process.platform;
return platform === 'win32'
? process.env.USERPROFILE
: process.env.HOME;
};
const result = getHomeDir();
expect(result).toBe('/home/testuser');
// Restore original platform
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
});
describe('select-file handler', () => {
it('should handle successful file selection', async () => {
const mockResult = {
canceled: false,
filePaths: ['/path/to/file.txt'],
};
mockDialog.showOpenDialog.mockResolvedValue(mockResult);
const result = await mockDialog.showOpenDialog({} as any, {
properties: ['openFile', 'multiSelections'],
});
expect(result.canceled).toBe(false);
expect(result.filePaths).toHaveLength(1);
});
it('should handle cancelled file selection', async () => {
const mockResult = {
canceled: true,
filePaths: [],
};
mockDialog.showOpenDialog.mockResolvedValue(mockResult);
const result = await mockDialog.showOpenDialog({} as any, {
properties: ['openFile', 'multiSelections'],
});
expect(result.canceled).toBe(true);
});
});
describe('read-file handler', () => {
it('should successfully read file', async () => {
const mockContent = 'file content';
mockFsp.readFile.mockResolvedValue(mockContent);
const result = await mockFsp.readFile('/path/to/file.txt', 'utf-8');
expect(result).toBe(mockContent);
});
it('should handle file read errors', async () => {
const error = new Error('File not found');
mockFsp.readFile.mockRejectedValue(error);
await expect(
mockFsp.readFile('/nonexistent/file.txt', 'utf-8')
).rejects.toThrow('File not found');
});
});
describe('export-log handler', () => {
it('should successfully export log file', async () => {
mockFsp.access.mockResolvedValue(undefined);
mockFsp.stat.mockResolvedValue({ size: 1000 });
mockFsp.readFile.mockResolvedValue('log content');
mockDialog.showSaveDialog.mockResolvedValue({
canceled: false,
filePath: '/path/to/exported.log',
});
mockFsp.writeFile.mockResolvedValue(undefined);
// Test the export log logic
await expect(
mockFsp.writeFile('/path/to/exported.log', 'log content', 'utf-8')
).resolves.toBeUndefined();
});
it('should handle empty log file', async () => {
mockFsp.access.mockResolvedValue(undefined);
mockFsp.stat.mockResolvedValue({ size: 0 });
const stats = await mockFsp.stat('/mock/log/path');
expect(stats.size).toBe(0);
});
it('should handle cancelled save dialog', async () => {
mockDialog.showSaveDialog.mockResolvedValue({
canceled: true,
filePath: undefined,
});
const result = await mockDialog.showSaveDialog({
title: 'save log file',
defaultPath: 'test.log',
filters: [{ name: 'log file', extensions: ['log', 'txt'] }],
});
expect(result.canceled).toBe(true);
});
});
describe('upload-log handler', () => {
it('should successfully upload log file', async () => {
const mockResponse = { status: 200, data: { success: true } };
mockAxios.post.mockResolvedValue(mockResponse);
const result = await mockAxios.post('/api/test', {});
expect(result.status).toBe(200);
});
it('should handle upload errors', async () => {
const error = new Error('Network error');
mockAxios.post.mockRejectedValue(error);
await expect(mockAxios.post('/api/test', {})).rejects.toThrow(
'Network error'
);
});
it('should validate required parameters', () => {
const validateParams = (
email: string,
taskId: string,
baseUrl: string,
token: string
) => {
if (!email || !taskId || !baseUrl || !token) {
throw new Error('Missing required parameters');
}
return true;
};
expect(() => validateParams('', 'task1', 'url', 'token')).toThrow(
'Missing required parameters'
);
expect(() => validateParams('email', '', 'url', 'token')).toThrow(
'Missing required parameters'
);
expect(() => validateParams('email', 'task1', '', 'token')).toThrow(
'Missing required parameters'
);
expect(() => validateParams('email', 'task1', 'url', '')).toThrow(
'Missing required parameters'
);
expect(validateParams('email', 'task1', 'url', 'token')).toBe(true);
});
it('should sanitize task ID', () => {
const sanitizeTaskId = (taskId: string) => {
return taskId.replace(/[^a-zA-Z0-9_-]/g, '');
};
expect(sanitizeTaskId('task_123')).toBe('task_123');
expect(sanitizeTaskId('task-456')).toBe('task-456');
expect(sanitizeTaskId('task@#$%')).toBe('task');
expect(sanitizeTaskId('task 123')).toBe('task123');
});
});
describe('window control handlers', () => {
it('should handle window close', () => {
const mockWin = { close: vi.fn() };
mockWin.close();
expect(mockWin.close).toHaveBeenCalled();
});
it('should handle window minimize', () => {
const mockWin = { minimize: vi.fn() };
mockWin.minimize();
expect(mockWin.minimize).toHaveBeenCalled();
});
it('should handle window maximize toggle', () => {
const mockWin = {
isMaximized: vi.fn(),
maximize: vi.fn(),
unmaximize: vi.fn(),
};
mockWin.isMaximized.mockReturnValue(false);
if (!mockWin.isMaximized()) {
mockWin.maximize();
}
expect(mockWin.maximize).toHaveBeenCalled();
mockWin.isMaximized.mockReturnValue(true);
if (mockWin.isMaximized()) {
mockWin.unmaximize();
}
expect(mockWin.unmaximize).toHaveBeenCalled();
});
});
});
describe('createWindow', () => {
it('should create window with correct configuration on macOS', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'darwin' });
const mockConfig = {
title: 'Eigent',
width: 1200,
height: 800,
minWidth: 1200,
minHeight: 800,
frame: false,
transparent: true,
vibrancy: 'sidebar',
visualEffectState: 'active',
backgroundColor: '#00000000',
titleBarStyle: 'hidden',
trafficLightPosition: { x: 10, y: 10 },
roundedCorners: true,
};
expect(mockConfig.titleBarStyle).toBe('hidden');
expect(mockConfig.trafficLightPosition).toEqual({ x: 10, y: 10 });
// Restore original platform
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should create window with correct configuration on Windows', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'win32' });
const mockConfig = {
title: 'Eigent',
width: 1200,
height: 800,
minWidth: 1200,
minHeight: 800,
frame: false,
transparent: true,
vibrancy: 'sidebar',
visualEffectState: 'active',
backgroundColor: '#00000000',
titleBarStyle: undefined,
trafficLightPosition: undefined,
roundedCorners: true,
};
expect(mockConfig.titleBarStyle).toBeUndefined();
expect(mockConfig.trafficLightPosition).toBeUndefined();
// Restore original platform
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
});
describe('setupWindowEventListeners', () => {
it('should set application menu to null', () => {
expect(mockMenu.setApplicationMenu).toBeDefined();
});
});
describe('setupDevToolsShortcuts', () => {
it('should handle F12 key for dev tools', () => {
const _mockEvent = { preventDefault: vi.fn() };
const mockInput = { key: 'F12', type: 'keyDown' };
expect(mockInput.key).toBe('F12');
expect(mockInput.type).toBe('keyDown');
});
it('should handle Ctrl+Shift+I for dev tools', () => {
const mockInput = {
control: true,
shift: true,
key: 'i',
type: 'keyDown',
};
const shouldToggle =
mockInput.control &&
mockInput.shift &&
mockInput.key.toLowerCase() === 'i' &&
mockInput.type === 'keyDown';
expect(shouldToggle).toBe(true);
});
it('should handle Cmd+Shift+I for dev tools on Mac', () => {
const mockInput = {
meta: true,
shift: true,
key: 'i',
type: 'keyDown',
};
const shouldToggle =
mockInput.meta &&
mockInput.shift &&
mockInput.key.toLowerCase() === 'i' &&
mockInput.type === 'keyDown';
expect(shouldToggle).toBe(true);
});
});
describe('setupExternalLinkHandling', () => {
it('should open external links in default browser', () => {
const mockUrl = 'https://example.com';
const shouldOpenExternal =
mockUrl.startsWith('https:') || mockUrl.startsWith('http:');
expect(shouldOpenExternal).toBe(true);
expect(mockShell.openExternal).toBeDefined();
});
it('should deny non-http(s) URLs', () => {
const mockUrl = 'file:///etc/passwd';
const shouldOpenExternal =
mockUrl.startsWith('https:') || mockUrl.startsWith('http:');
expect(shouldOpenExternal).toBe(false);
});
});
describe('cleanupPythonProcess', () => {
it('should cleanup python process successfully', async () => {
const mockProcess = {
pid: 1234,
kill: vi.fn(),
};
// Test cleanup logic
if (mockProcess) {
mockProcess.kill();
}
expect(mockProcess.kill).toHaveBeenCalled();
});
it('should handle cleanup errors gracefully', async () => {
const mockKill = vi.fn().mockImplementation((pid, callback) => {
callback(new Error('Process not found'));
});
// Test error handling in cleanup
expect(() => {
mockKill(1234, (error: Error) => {
if (error) throw error;
});
}).toThrow('Process not found');
});
});
describe('handleDependencyInstallation', () => {
it('should install dependencies successfully', async () => {
const mockInstallDependencies = vi.fn().mockResolvedValue(true);
const _mockCheckToolInstalled = vi.fn().mockResolvedValue(true);
const _mockStartBackend = vi.fn().mockResolvedValue({ pid: 1234 });
const result = await mockInstallDependencies();
expect(result).toBe(true);
});
it('should handle installation failure', async () => {
const mockInstallDependencies = vi.fn().mockResolvedValue(false);
const result = await mockInstallDependencies();
expect(result).toBe(false);
});
it('should start backend when tool is installed', async () => {
const mockCheckToolInstalled = vi.fn().mockResolvedValue(true);
const mockStartBackend = vi.fn().mockResolvedValue({ pid: 1234 });
const isToolInstalled = await mockCheckToolInstalled();
if (isToolInstalled) {
const process = await mockStartBackend(() => {});
expect(process).toEqual({ pid: 1234 });
}
});
it('should skip backend start when tool is not installed', async () => {
const mockCheckToolInstalled = vi.fn().mockResolvedValue(false);
const isToolInstalled = await mockCheckToolInstalled();
expect(isToolInstalled).toBe(false);
});
});
describe('Browser Path Constants', () => {
it('should define correct Windows browser paths', () => {
const BROWSER_PATHS = {
win32: {
chrome: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
edge: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
firefox: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe',
},
};
expect(BROWSER_PATHS.win32.chrome).toContain('chrome.exe');
expect(BROWSER_PATHS.win32.edge).toContain('msedge.exe');
expect(BROWSER_PATHS.win32.firefox).toContain('firefox.exe');
});
it('should define correct macOS browser paths', () => {
const BROWSER_PATHS = {
darwin: {
chrome: '/Applications/Google Chrome.app',
edge: '/Applications/Microsoft Edge.app',
firefox: '/Applications/Firefox.app',
safari: '/Applications/Safari.app',
},
};
expect(BROWSER_PATHS.darwin.chrome).toContain('.app');
expect(BROWSER_PATHS.darwin.edge).toContain('.app');
expect(BROWSER_PATHS.darwin.firefox).toContain('.app');
expect(BROWSER_PATHS.darwin.safari).toContain('.app');
});
});
describe('App Event Handlers', () => {
it('should handle app ready event', () => {
expect(mockApp.whenReady).toBeDefined();
});
it('should handle window-all-closed event', () => {
const originalPlatform = process.platform;
// Test non-darwin platform
Object.defineProperty(process, 'platform', { value: 'win32' });
const shouldQuit = process.platform !== 'darwin';
expect(shouldQuit).toBe(true);
// Test darwin platform
Object.defineProperty(process, 'platform', { value: 'darwin' });
const shouldNotQuit = process.platform !== 'darwin';
expect(shouldNotQuit).toBe(false);
// Restore original platform
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should handle app activate event', () => {
const mockWindows = [{ show: vi.fn() }];
if (mockWindows.length > 0) {
mockWindows[0].show();
expect(mockWindows[0].show).toHaveBeenCalled();
}
});
it('should handle before-quit event', () => {
const mockProcess = { pid: 1234 };
// Test cleanup logic
if (mockProcess) {
expect(mockProcess.pid).toBe(1234);
}
});
});
describe('Environment and Platform Detection', () => {
it('should detect Windows 7 and disable hardware acceleration', () => {
mockOs.release.mockReturnValue('6.1.7601');
const release = mockOs.release();
const isWindows7 = release.startsWith('6.1');
expect(isWindows7).toBe(true);
});
it('should not disable hardware acceleration on newer Windows', () => {
mockOs.release.mockReturnValue('10.0.19041');
const release = mockOs.release();
const isWindows7 = release.startsWith('6.1');
expect(isWindows7).toBe(false);
});
it('should set app user model ID on Windows', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'win32' });
const isWindows = process.platform === 'win32';
expect(isWindows).toBe(true);
// Restore original platform
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
});
describe('Constants and Paths', () => {
it('should define correct path constants', () => {
const mockDirname = '/app/dist-electron/main';
const mockPath = {
join: (dir: string, ...paths: string[]) => {
const allPaths = [dir, ...paths];
return allPaths.join('/').replace(/\/+/g, '/');
},
};
const MAIN_DIST = mockPath.join(mockDirname, '../..');
const RENDERER_DIST = mockPath.join(MAIN_DIST, 'dist');
expect(MAIN_DIST).toContain('dist-electron');
expect(RENDERER_DIST).toContain('dist');
});
it('should handle VITE_DEV_SERVER_URL correctly', () => {
const VITE_DEV_SERVER_URL = 'http://localhost:3000';
const MAIN_DIST = '/app';
const mockPath = {
join: (dir: string, ...paths: string[]) => `${dir}/${paths.join('/')}`,
};
const VITE_PUBLIC = VITE_DEV_SERVER_URL
? mockPath.join(MAIN_DIST, 'public')
: mockPath.join(MAIN_DIST, 'dist');
expect(VITE_PUBLIC).toContain('public');
// Test when no dev server URL
const VITE_PUBLIC_PROD = !VITE_DEV_SERVER_URL
? mockPath.join(MAIN_DIST, 'public')
: mockPath.join(MAIN_DIST, 'dist');
expect(VITE_PUBLIC_PROD).toContain('dist');
});
});
describe('MCP Handlers', () => {
it('should handle mcp-install', () => {
const mockHandler = vi.fn((_event, name, mcp) => {
mockedMcpConfig.addMcp(name, mcp);
return { success: true };
});
mockIpcMain.handle('mcp-install', mockHandler);
mockHandler({}, 'test-mcp', { data: 'data' });
expect(mockedMcpConfig.addMcp).toHaveBeenCalledWith('test-mcp', {
data: 'data',
});
});
it('should handle mcp-remove', () => {
const mockHandler = vi.fn((_event, name) => {
mockedMcpConfig.removeMcp(name);
return { success: true };
});
mockIpcMain.handle('mcp-remove', mockHandler);
mockHandler({}, 'test-mcp');
expect(mockedMcpConfig.removeMcp).toHaveBeenCalledWith('test-mcp');
});
it('should handle mcp-update', () => {
const mockHandler = vi.fn((_event, name, mcp) => {
mockedMcpConfig.updateMcp(name, mcp);
return { success: true };
});
mockIpcMain.handle('mcp-update', mockHandler);
mockHandler({}, 'test-mcp', { data: 'new-data' });
expect(mockedMcpConfig.updateMcp).toHaveBeenCalledWith('test-mcp', {
data: 'new-data',
});
});
it('should handle mcp-list', async () => {
const mockData = { mcp1: { version: '1.0' } };
mockedMcpConfig.readMcpConfig.mockResolvedValue(mockData);
const mockHandler = vi.fn(() => mockedMcpConfig.readMcpConfig());
mockIpcMain.handle('mcp-list', mockHandler);
const result = await mockHandler();
expect(mockedMcpConfig.readMcpConfig).toHaveBeenCalled();
expect(result).toEqual(mockData);
});
});
describe('Environment Variable Handlers', () => {
beforeEach(() => {
mockedEnvUtil.getEnvPath.mockReturnValue('/mock/env/path/.env');
});
it('should handle get-env-path', async () => {
const mockHandler = vi.fn((_event, email) =>
mockedEnvUtil.getEnvPath(email)
);
mockIpcMain.handle('get-env-path', mockHandler);
const result = await mockHandler({}, 'test@example.com');
expect(mockedEnvUtil.getEnvPath).toHaveBeenCalledWith('test@example.com');
expect(result).toBe('/mock/env/path/.env');
});
it('should handle env-write', async () => {
const mockHandler = vi.fn(async (_event, email, { key, value }) => {
const ENV_PATH = mockedEnvUtil.getEnvPath(email);
mockFs.readFileSync.mockReturnValue('EXISTING_KEY=old_value');
let lines = mockFs.readFileSync(ENV_PATH, 'utf-8').split(/\r?\n/);
// Mock updateEnvBlock to return an array
mockedEnvUtil.updateEnvBlock.mockReturnValue([
'EXISTING_KEY=old_value',
'NEW_KEY=new_value',
]);
lines = mockedEnvUtil.updateEnvBlock(lines, { [key]: value });
mockFs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8');
return { success: true };
});
mockIpcMain.handle('env-write', mockHandler);
await mockHandler({}, 'test@example.com', {
key: 'NEW_KEY',
value: 'new_value',
});
expect(mockFs.writeFileSync).toHaveBeenCalled();
});
it('should handle env-remove', async () => {
const mockHandler = vi.fn(async (_event, email, key) => {
const ENV_PATH = mockedEnvUtil.getEnvPath(email);
mockFs.readFileSync.mockReturnValue(
'KEY_TO_REMOVE=some_value\nOTHER_KEY=other_value'
);
let lines = mockFs.readFileSync(ENV_PATH, 'utf-8').split(/\r?\n/);
// Mock removeEnvKey to return an array
mockedEnvUtil.removeEnvKey.mockReturnValue(['OTHER_KEY=other_value']);
lines = mockedEnvUtil.removeEnvKey(lines, key);
mockFs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8');
return { success: true };
});
mockIpcMain.handle('env-remove', mockHandler);
await mockHandler({}, 'test@example.com', 'KEY_TO_REMOVE');
expect(mockFs.writeFileSync).toHaveBeenCalled();
});
});
describe('File and Folder Handlers', () => {
it('should handle reveal-in-folder', () => {
const mockHandler = vi.fn((_event, filePath) => {
mockShell.showItemInFolder(filePath);
});
mockIpcMain.handle('reveal-in-folder', mockHandler);
mockHandler({}, '/path/to/file');
expect(mockShell.showItemInFolder).toHaveBeenCalledWith('/path/to/file');
});
it('should handle delete-folder successfully', async () => {
mockedEnvUtil.getEmailFolderPath.mockReturnValue({
MCP_REMOTE_CONFIG_DIR: '/mock/mcp/dir',
MCP_CONFIG_DIR: '',
tempEmail: '',
hasToken: false,
});
mockFs.existsSync.mockReturnValue(true);
mockFsp.stat.mockResolvedValue({ isDirectory: () => true } as any);
mockFsp.rm.mockResolvedValue(undefined);
const mockHandler = vi.fn(async (_event, email) => {
const { MCP_REMOTE_CONFIG_DIR } =
mockedEnvUtil.getEmailFolderPath(email);
if (!mockFs.existsSync(MCP_REMOTE_CONFIG_DIR)) {
return { success: false, error: 'Folder does not exist' };
}
const stats = await mockFsp.stat(MCP_REMOTE_CONFIG_DIR);
if (!stats.isDirectory()) {
return { success: false, error: 'Path is not a directory' };
}
await mockFsp.rm(MCP_REMOTE_CONFIG_DIR, {
recursive: true,
force: true,
});
return { success: true };
});
mockIpcMain.handle('delete-folder', mockHandler);
const result = await mockHandler({}, 'test@example.com');
expect(mockedEnvUtil.getEmailFolderPath).toHaveBeenCalledWith(
'test@example.com'
);
expect(mockFsp.rm).toHaveBeenCalledWith('/mock/mcp/dir', {
recursive: true,
force: true,
});
expect(result).toEqual({ success: true });
});
it('should handle delete-folder when folder does not exist', async () => {
mockedEnvUtil.getEmailFolderPath.mockReturnValue({
MCP_REMOTE_CONFIG_DIR: '/mock/mcp/dir',
MCP_CONFIG_DIR: '',
tempEmail: '',
hasToken: false,
});
mockFs.existsSync.mockReturnValue(false);
const mockHandler = vi.fn(async (_event, email) => {
const { MCP_REMOTE_CONFIG_DIR } =
mockedEnvUtil.getEmailFolderPath(email);
if (!mockFs.existsSync(MCP_REMOTE_CONFIG_DIR)) {
return { success: false, error: 'Folder does not exist' };
}
//...
});
mockIpcMain.handle('delete-folder', mockHandler);
const result = await mockHandler({}, 'test@example.com');
expect(result).toEqual({
success: false,
error: 'Folder does not exist',
});
});
});
describe('Backend and Dependency Handlers', () => {
it('should handle check-tool-installed', async () => {
mockedInitModule.checkToolInstalled.mockResolvedValue(true);
const mockHandler = vi.fn(async () => {
const isInstalled = await mockedInitModule.checkToolInstalled();
return { success: true, isInstalled };
});
mockIpcMain.handle('check-tool-installed', mockHandler);
const result = await mockHandler();
expect(mockedInitModule.checkToolInstalled).toHaveBeenCalled();
expect(result).toEqual({ success: true, isInstalled: true });
});
it('should handle installation triggering', async () => {
// Create a mock handler that actually calls installDependencies
const mockHandler = vi.fn(async () => {
const result = await mockedInitModule.installDependencies();
return result;
});
mockIpcMain.handle('install-dependencies', mockHandler);
mockIpcMain.handle('frontend-ready', mockHandler);
mockedInitModule.installDependencies.mockResolvedValue(true);
await mockHandler();
expect(mockedInitModule.installDependencies).toHaveBeenCalled();
});
});
describe('FileReader and WebViewManager Handlers', () => {
let mockFileReader: any;
let mockWebViewManager: any;
beforeEach(() => {
mockFileReader = {
openFile: vi.fn(),
getFileList: vi.fn(),
};
mockWebViewManager = {
captureWebview: vi.fn(),
createWebview: vi.fn(),
hideWebview: vi.fn(),
showWebview: vi.fn(),
changeViewSize: vi.fn(),
hideAllWebview: vi.fn(),
getActiveWebview: vi.fn(),
setSize: vi.fn(),
getShowWebview: vi.fn(),
destroyWebview: vi.fn(),
};
// Mock the managers being available
vi.doMock('../../../../electron/main/fileReader', () => ({
FileReader: vi.fn(() => mockFileReader),
}));
vi.doMock('../../../../electron/main/webview', () => ({
WebViewManager: vi.fn(() => mockWebViewManager),
}));
});
it('should handle open-file', async () => {
const mockHandler = vi.fn((...args) => mockFileReader.openFile(...args));
mockIpcMain.handle('open-file', mockHandler);
await mockHandler('type', 'path', true);
expect(mockFileReader.openFile).toHaveBeenCalledWith(
'type',
'path',
true
);
});
it('should handle get-file-list', async () => {
const mockHandler = vi.fn((...args) =>
mockFileReader.getFileList(...args)
);
mockIpcMain.handle('get-file-list', mockHandler);
await mockHandler('email', 'taskId');
expect(mockFileReader.getFileList).toHaveBeenCalledWith(
'email',
'taskId'
);
});
it('should handle create-webview', async () => {
const mockHandler = vi.fn((...args) =>
mockWebViewManager.createWebview(...args)
);
mockIpcMain.handle('create-webview', mockHandler);
await mockHandler('id');
expect(mockWebViewManager.createWebview).toHaveBeenCalledWith('id');
});
it('should handle webview-destroy', async () => {
const mockHandler = vi.fn((...args) =>
mockWebViewManager.destroyWebview(...args)
);
mockIpcMain.handle('webview-destroy', mockHandler);
await mockHandler();
expect(mockWebViewManager.destroyWebview).toHaveBeenCalled();
});
});
describe('localfile:// Protocol Path Traversal Prevention', () => {
/**
* Tests for the path validation logic in the localfile:// protocol handler.
* Without validation, path.normalize() does NOT prevent directory traversal,
* allowing requests like localfile:///../../../etc/passwd to read arbitrary files.
*/
const path = require('node:path');
const isPathAllowed = (filePath: string, allowedBases: string[]): boolean => {
const resolvedPath = path.resolve(filePath);
return allowedBases.some((base: string) => {
const resolvedBase = path.resolve(base);
return (
resolvedPath === resolvedBase ||
resolvedPath.startsWith(resolvedBase + path.sep)
);
});
};
const ALLOWED_BASES = ['/home/user', '/mock/user/data', '/tmp'];
it('should allow files within home directory', () => {
expect(isPathAllowed('/home/user/documents/file.pdf', ALLOWED_BASES)).toBe(true);
});
it('should allow files within userData directory', () => {
expect(isPathAllowed('/mock/user/data/cache/image.png', ALLOWED_BASES)).toBe(true);
});
it('should allow files within temp directory', () => {
expect(isPathAllowed('/tmp/upload-123.txt', ALLOWED_BASES)).toBe(true);
});
it('should block path traversal to /etc/passwd', () => {
const traversalPath = path.resolve(
path.normalize('/home/user/.eigent/data/../../../etc/passwd')
);
expect(isPathAllowed(traversalPath, ALLOWED_BASES)).toBe(false);
});
it('should block absolute paths outside allowed directories', () => {
expect(isPathAllowed('/etc/shadow', ALLOWED_BASES)).toBe(false);
expect(isPathAllowed('/var/log/syslog', ALLOWED_BASES)).toBe(false);
expect(isPathAllowed('/root/.ssh/id_rsa', ALLOWED_BASES)).toBe(false);
});
it('should block encoded traversal after normalize', () => {
// Simulate what happens after decodeURIComponent + normalize
const decodedUrl = '/home/user/data/../../../../etc/passwd';
const normalized = path.normalize(decodedUrl);
const resolved = path.resolve(normalized);
expect(isPathAllowed(resolved, ALLOWED_BASES)).toBe(false);
});
it('should allow exact base directory path', () => {
expect(isPathAllowed('/home/user', ALLOWED_BASES)).toBe(true);
});
it('should block paths that are prefixes but not subdirectories', () => {
// /home/user-evil should NOT match /home/user
expect(isPathAllowed('/home/user-evil/file.txt', ALLOWED_BASES)).toBe(false);
});
});
describe('Application Lifecycle', () => {
it('should quit on window-all-closed for non-darwin platforms', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'win32' });
mockApp.on.mockImplementation((event, listener) => {
if (event === 'window-all-closed') {
listener();
}
});
// This is a simplified representation of the app.on('window-all-closed') logic
const windowAllClosedHandler = () => {
if (process.platform !== 'darwin') {
mockApp.quit();
}
};
windowAllClosedHandler();
expect(mockApp.quit).toHaveBeenCalled();
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should not quit on window-all-closed for darwin platforms', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { value: 'darwin' });
vi.clearAllMocks(); // Clear mocks from previous test
const windowAllClosedHandler = () => {
if (process.platform !== 'darwin') {
mockApp.quit();
}
};
windowAllClosedHandler();
expect(mockApp.quit).not.toHaveBeenCalled();
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should call cleanup on before-quit', () => {
const mockCleanup = vi.fn();
mockApp.on.mockImplementation((event, listener) => {
if (event === 'before-quit') {
listener();
}
});
const beforeQuitHandler = () => {
mockCleanup();
};
beforeQuitHandler();
expect(mockCleanup).toHaveBeenCalled();
});
});
});