Files
Maestro/src/__tests__/main/web-server/web-server-factory.test.ts
Raza Rauf 68945cb946 refactor: restructure web-server module with security and memory leak fixes
- Move WebServer class to dedicated file and add module index
- Extract shared types to centralized types.ts
- Fix XSS vulnerability by sanitizing sessionId/tabId in URL parameters
- Fix IPC listener memory leak with proper cleanup on timeout
- Add autoRunStates cleanup when sessions go offline
- Refactor message handlers with send() and sendError() helpers
- Add XSS sanitization tests and e2e test configuration
2026-01-29 15:27:28 -05:00

471 lines
14 KiB
TypeScript

/**
* @file web-server-factory.test.ts
* @description Unit tests for web server factory with dependency injection.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { BrowserWindow, WebContents } from 'electron';
// Mock electron
vi.mock('electron', () => ({
ipcMain: {
once: vi.fn(),
},
}));
// Mock WebServer - use class syntax to make it a proper constructor
// Note: Mock the specific file path that web-server-factory.ts imports from
vi.mock('../../../main/web-server/WebServer', () => {
return {
WebServer: class MockWebServer {
port: number;
setGetSessionsCallback = vi.fn();
setGetSessionDetailCallback = vi.fn();
setGetThemeCallback = vi.fn();
setGetCustomCommandsCallback = vi.fn();
setGetHistoryCallback = vi.fn();
setWriteToSessionCallback = vi.fn();
setExecuteCommandCallback = vi.fn();
setInterruptSessionCallback = vi.fn();
setSwitchModeCallback = vi.fn();
setSelectSessionCallback = vi.fn();
setSelectTabCallback = vi.fn();
setNewTabCallback = vi.fn();
setCloseTabCallback = vi.fn();
setRenameTabCallback = vi.fn();
constructor(port: number) {
this.port = port;
}
},
};
});
// Mock themes
vi.mock('../../../main/themes', () => ({
getThemeById: vi.fn().mockReturnValue({ id: 'dracula', name: 'Dracula' }),
}));
// Mock history manager
vi.mock('../../../main/history-manager', () => ({
getHistoryManager: vi.fn().mockReturnValue({
getEntries: vi.fn().mockReturnValue([]),
getEntriesByProjectPath: vi.fn().mockReturnValue([]),
getAllEntries: vi.fn().mockReturnValue([]),
}),
}));
// Mock logger
vi.mock('../../../main/utils/logger', () => ({
logger: {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
import {
createWebServerFactory,
type WebServerFactoryDependencies,
} from '../../../main/web-server/web-server-factory';
import { WebServer } from '../../../main/web-server/WebServer';
import { getThemeById } from '../../../main/themes';
import { getHistoryManager } from '../../../main/history-manager';
import { logger } from '../../../main/utils/logger';
describe('web-server/web-server-factory', () => {
let mockSettingsStore: WebServerFactoryDependencies['settingsStore'];
let mockSessionsStore: WebServerFactoryDependencies['sessionsStore'];
let mockGroupsStore: WebServerFactoryDependencies['groupsStore'];
let mockMainWindow: Partial<BrowserWindow>;
let mockWebContents: Partial<WebContents>;
let mockProcessManager: { write: ReturnType<typeof vi.fn> };
let deps: WebServerFactoryDependencies;
beforeEach(() => {
vi.clearAllMocks();
mockSettingsStore = {
get: vi.fn((key: string, defaultValue?: any) => {
const values: Record<string, any> = {
webInterfaceUseCustomPort: false,
webInterfaceCustomPort: 8080,
activeThemeId: 'dracula',
customAICommands: [],
};
return values[key] ?? defaultValue;
}),
};
mockSessionsStore = {
get: vi.fn((key: string, defaultValue?: any) => {
if (key === 'sessions') {
return [
{
id: 'session-1',
name: 'Test Session',
toolType: 'claude-code',
state: 'idle',
inputMode: 'ai',
cwd: '/test/path',
aiTabs: [
{
id: 'tab-1',
logs: [{ source: 'stdout', text: 'Hello', timestamp: Date.now() }],
},
],
activeTabId: 'tab-1',
},
];
}
return defaultValue;
}),
};
mockGroupsStore = {
get: vi.fn((key: string, defaultValue?: any) => {
if (key === 'groups') {
return [{ id: 'group-1', name: 'Test Group', emoji: '🧪' }];
}
return defaultValue;
}),
};
mockWebContents = {
send: vi.fn(),
};
mockMainWindow = {
webContents: mockWebContents as WebContents,
};
mockProcessManager = {
write: vi.fn().mockReturnValue(true),
};
deps = {
settingsStore: mockSettingsStore,
sessionsStore: mockSessionsStore,
groupsStore: mockGroupsStore,
getMainWindow: vi.fn().mockReturnValue(mockMainWindow as BrowserWindow),
getProcessManager: vi.fn().mockReturnValue(mockProcessManager),
};
});
describe('createWebServerFactory', () => {
it('should return a function', () => {
const factory = createWebServerFactory(deps);
expect(typeof factory).toBe('function');
});
it('should create a WebServer when called', () => {
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
expect(server).toBeDefined();
expect(server).toBeInstanceOf(WebServer);
});
it('should use random port (0) when custom port is disabled', () => {
vi.mocked(mockSettingsStore.get).mockImplementation((key: string, defaultValue?: any) => {
if (key === 'webInterfaceUseCustomPort') return false;
if (key === 'webInterfaceCustomPort') return 9999;
return defaultValue;
});
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
// Check that the server was created with port 0 (random)
expect((server as any).port).toBe(0);
});
it('should use custom port when enabled', () => {
vi.mocked(mockSettingsStore.get).mockImplementation((key: string, defaultValue?: any) => {
if (key === 'webInterfaceUseCustomPort') return true;
if (key === 'webInterfaceCustomPort') return 9999;
return defaultValue;
});
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
// Check that the server was created with custom port
expect((server as any).port).toBe(9999);
});
});
describe('callback registrations', () => {
let createWebServer: ReturnType<typeof createWebServerFactory>;
let server: ReturnType<typeof createWebServer>;
beforeEach(() => {
createWebServer = createWebServerFactory(deps);
server = createWebServer();
});
it('should register getSessionsCallback', () => {
expect(server.setGetSessionsCallback).toHaveBeenCalled();
});
it('should register getSessionDetailCallback', () => {
expect(server.setGetSessionDetailCallback).toHaveBeenCalled();
});
it('should register getThemeCallback', () => {
expect(server.setGetThemeCallback).toHaveBeenCalled();
});
it('should register getCustomCommandsCallback', () => {
expect(server.setGetCustomCommandsCallback).toHaveBeenCalled();
});
it('should register getHistoryCallback', () => {
expect(server.setGetHistoryCallback).toHaveBeenCalled();
});
it('should register writeToSessionCallback', () => {
expect(server.setWriteToSessionCallback).toHaveBeenCalled();
});
it('should register executeCommandCallback', () => {
expect(server.setExecuteCommandCallback).toHaveBeenCalled();
});
it('should register interruptSessionCallback', () => {
expect(server.setInterruptSessionCallback).toHaveBeenCalled();
});
it('should register switchModeCallback', () => {
expect(server.setSwitchModeCallback).toHaveBeenCalled();
});
it('should register selectSessionCallback', () => {
expect(server.setSelectSessionCallback).toHaveBeenCalled();
});
it('should register tab operation callbacks', () => {
expect(server.setSelectTabCallback).toHaveBeenCalled();
expect(server.setNewTabCallback).toHaveBeenCalled();
expect(server.setCloseTabCallback).toHaveBeenCalled();
expect(server.setRenameTabCallback).toHaveBeenCalled();
});
});
describe('getSessionsCallback behavior', () => {
it('should return sessions with mapped data', () => {
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
// Get the callback that was registered
const setGetSessionsCallback = server.setGetSessionsCallback as ReturnType<typeof vi.fn>;
const callback = setGetSessionsCallback.mock.calls[0][0];
const sessions = callback();
expect(Array.isArray(sessions)).toBe(true);
expect(sessions.length).toBeGreaterThan(0);
expect(sessions[0]).toHaveProperty('id');
expect(sessions[0]).toHaveProperty('name');
expect(sessions[0]).toHaveProperty('toolType');
});
});
describe('writeToSessionCallback behavior', () => {
it('should return false when processManager is null', () => {
deps.getProcessManager = vi.fn().mockReturnValue(null);
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setWriteCallback = server.setWriteToSessionCallback as ReturnType<typeof vi.fn>;
const callback = setWriteCallback.mock.calls[0][0];
const result = callback('session-1', 'test data');
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it('should return false when session not found', () => {
vi.mocked(mockSessionsStore.get).mockReturnValue([]);
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setWriteCallback = server.setWriteToSessionCallback as ReturnType<typeof vi.fn>;
const callback = setWriteCallback.mock.calls[0][0];
const result = callback('non-existent-session', 'test data');
expect(result).toBe(false);
});
it('should write to AI process when inputMode is ai', () => {
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setWriteCallback = server.setWriteToSessionCallback as ReturnType<typeof vi.fn>;
const callback = setWriteCallback.mock.calls[0][0];
callback('session-1', 'test data');
expect(mockProcessManager.write).toHaveBeenCalledWith('session-1-ai', 'test data');
});
});
describe('executeCommandCallback behavior', () => {
it('should return false when mainWindow is null', async () => {
deps.getMainWindow = vi.fn().mockReturnValue(null);
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setExecuteCallback = server.setExecuteCommandCallback as ReturnType<typeof vi.fn>;
const callback = setExecuteCallback.mock.calls[0][0];
const result = await callback('session-1', 'test command');
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it('should send command to renderer', async () => {
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setExecuteCallback = server.setExecuteCommandCallback as ReturnType<typeof vi.fn>;
const callback = setExecuteCallback.mock.calls[0][0];
const result = await callback('session-1', 'test command', 'ai');
expect(result).toBe(true);
expect(mockWebContents.send).toHaveBeenCalledWith(
'remote:executeCommand',
'session-1',
'test command',
'ai'
);
});
});
describe('interruptSessionCallback behavior', () => {
it('should return false when mainWindow is null', async () => {
deps.getMainWindow = vi.fn().mockReturnValue(null);
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setInterruptCallback = server.setInterruptSessionCallback as ReturnType<typeof vi.fn>;
const callback = setInterruptCallback.mock.calls[0][0];
const result = await callback('session-1');
expect(result).toBe(false);
});
it('should send interrupt to renderer', async () => {
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setInterruptCallback = server.setInterruptSessionCallback as ReturnType<typeof vi.fn>;
const callback = setInterruptCallback.mock.calls[0][0];
const result = await callback('session-1');
expect(result).toBe(true);
expect(mockWebContents.send).toHaveBeenCalledWith('remote:interrupt', 'session-1');
});
});
describe('switchModeCallback behavior', () => {
it('should send mode switch to renderer', async () => {
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setSwitchModeCallback = server.setSwitchModeCallback as ReturnType<typeof vi.fn>;
const callback = setSwitchModeCallback.mock.calls[0][0];
const result = await callback('session-1', 'terminal');
expect(result).toBe(true);
expect(mockWebContents.send).toHaveBeenCalledWith(
'remote:switchMode',
'session-1',
'terminal'
);
});
});
describe('getThemeCallback behavior', () => {
it('should return theme from getThemeById', () => {
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setThemeCallback = server.setGetThemeCallback as ReturnType<typeof vi.fn>;
const callback = setThemeCallback.mock.calls[0][0];
const theme = callback();
expect(getThemeById).toHaveBeenCalled();
expect(theme).toEqual({ id: 'dracula', name: 'Dracula' });
});
});
describe('getHistoryCallback behavior', () => {
it('should get entries for specific session', () => {
const mockHistoryManager = {
getEntries: vi.fn().mockReturnValue([{ id: 1 }]),
getEntriesByProjectPath: vi.fn(),
getAllEntries: vi.fn(),
};
vi.mocked(getHistoryManager).mockReturnValue(mockHistoryManager as any);
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setHistoryCallback = server.setGetHistoryCallback as ReturnType<typeof vi.fn>;
const callback = setHistoryCallback.mock.calls[0][0];
callback(undefined, 'session-1');
expect(mockHistoryManager.getEntries).toHaveBeenCalledWith('session-1');
});
it('should get entries by project path', () => {
const mockHistoryManager = {
getEntries: vi.fn(),
getEntriesByProjectPath: vi.fn().mockReturnValue([{ id: 1 }]),
getAllEntries: vi.fn(),
};
vi.mocked(getHistoryManager).mockReturnValue(mockHistoryManager as any);
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setHistoryCallback = server.setGetHistoryCallback as ReturnType<typeof vi.fn>;
const callback = setHistoryCallback.mock.calls[0][0];
callback('/test/project');
expect(mockHistoryManager.getEntriesByProjectPath).toHaveBeenCalledWith('/test/project');
});
it('should get all entries when no filter', () => {
const mockHistoryManager = {
getEntries: vi.fn(),
getEntriesByProjectPath: vi.fn(),
getAllEntries: vi.fn().mockReturnValue([{ id: 1 }]),
};
vi.mocked(getHistoryManager).mockReturnValue(mockHistoryManager as any);
const createWebServer = createWebServerFactory(deps);
const server = createWebServer();
const setHistoryCallback = server.setGetHistoryCallback as ReturnType<typeof vi.fn>;
const callback = setHistoryCallback.mock.calls[0][0];
callback();
expect(mockHistoryManager.getAllEntries).toHaveBeenCalled();
});
});
});