import { jest } from '@jest/globals'; // Mock all external dependencies before importing the module const mockMkdir = jest.fn(); const mockReadFile = jest.fn(); const mockWriteFileSync = jest.fn(); const mockReadFileSync = jest.fn(); const mockUnlinkSync = jest.fn(); const mockExistsSync = jest.fn(); const mockReaddirSync = jest.fn(); jest.unstable_mockModule('fs/promises', () => ({ mkdir: mockMkdir, readFile: mockReadFile })); jest.unstable_mockModule('fs', () => ({ writeFileSync: mockWriteFileSync, readFileSync: mockReadFileSync, unlinkSync: mockUnlinkSync, existsSync: mockExistsSync, readdirSync: mockReaddirSync })); const mockJoin = jest.fn(); const mockDirname = jest.fn(); const mockBasename = jest.fn(); jest.unstable_mockModule('path', () => ({ join: mockJoin, dirname: mockDirname, basename: mockBasename })); const mockFileURLToPath = jest.fn(); jest.unstable_mockModule('url', () => ({ fileURLToPath: mockFileURLToPath })); const mockSecureImportModule = jest.fn(); jest.unstable_mockModule('../dist/utils/plugins.js', () => ({ secureImportModule: mockSecureImportModule })); const mockLogs = { section: jest.fn(), init: jest.fn(), config: jest.fn(), error: jest.fn(), msg: jest.fn(), server: jest.fn() }; jest.unstable_mockModule('../dist/utils/logs.js', () => mockLogs); const mockApp = { set: jest.fn(), use: jest.fn(), listen: jest.fn() }; const mockExpress = jest.fn(() => mockApp); mockExpress.json = jest.fn(() => (req, res, next) => next()); mockExpress.urlencoded = jest.fn(() => (req, res, next) => next()); mockExpress.static = jest.fn(() => (req, res, next) => next()); mockExpress.Router = jest.fn(() => ({ use: jest.fn() })); jest.unstable_mockModule('express', () => ({ default: mockExpress })); const mockServer = { on: jest.fn(), close: jest.fn(), listen: jest.fn() }; const mockCreateServer = jest.fn(() => mockServer); jest.unstable_mockModule('http', () => ({ createServer: mockCreateServer })); const mockSpawn = jest.fn(); jest.unstable_mockModule('child_process', () => ({ spawn: mockSpawn })); jest.unstable_mockModule('dotenv', () => ({ config: jest.fn() })); // Mock process for command line argument testing const originalArgv = process.argv; const originalEnv = process.env; const originalExit = process.exit; const originalKill = process.kill; const originalOn = process.on; // Mock imports properly for ES modules const mockTomlModule = { parse: jest.fn(() => ({})) }; jest.unstable_mockModule('@iarna/toml', () => ({ default: mockTomlModule })); // Import the module after all mocking is set up let indexModule; describe('Index (Main Application)', () => { let mockProcess; beforeAll(async () => { // Import the module once with proper mocking setup indexModule = await import('../dist/index.js'); }); beforeEach(() => { // Reset all mocks jest.clearAllMocks(); // Setup default mock returns mockFileURLToPath.mockReturnValue('/app/src/index.js'); mockDirname.mockReturnValue('/app/src'); mockJoin.mockImplementation((...parts) => parts.join('/')); mockBasename.mockImplementation((path, ext) => { const name = path.split('/').pop(); return ext ? name.replace(ext, '') : name; }); mockExistsSync.mockReturnValue(false); mockReaddirSync.mockReturnValue([]); mockReadFile.mockResolvedValue(''); mockTomlModule.parse.mockReturnValue({}); // Setup process mocking mockProcess = { argv: ['node', 'index.js'], env: { ...originalEnv }, exit: jest.fn(), kill: jest.fn(), on: jest.fn() }; // Note: Plugin registry persists across tests in ES modules // This is expected behavior in the test environment }); afterEach(() => { process.argv = originalArgv; process.env = originalEnv; process.exit = originalExit; process.kill = originalKill; process.on = originalOn; }); describe('Plugin Registration System', () => { test('should register plugin successfully', () => { const pluginName = 'test-plugin'; const handler = { middleware: jest.fn() }; indexModule.registerPlugin(pluginName, handler); const registeredNames = indexModule.getRegisteredPluginNames(); expect(registeredNames).toContain(pluginName); }); test('should throw error for invalid plugin name', () => { expect(() => { indexModule.registerPlugin('', { middleware: jest.fn() }); }).toThrow('Plugin name must be a non-empty string'); expect(() => { indexModule.registerPlugin(null, { middleware: jest.fn() }); }).toThrow('Plugin name must be a non-empty string'); }); test('should throw error for invalid handler', () => { expect(() => { indexModule.registerPlugin('test', null); }).toThrow('Plugin handler must be an object'); expect(() => { indexModule.registerPlugin('test', 'invalid'); }).toThrow('Plugin handler must be an object'); }); test('should prevent duplicate plugin registration', () => { const pluginName = 'duplicate-plugin'; const handler = { middleware: jest.fn() }; indexModule.registerPlugin(pluginName, handler); expect(() => { indexModule.registerPlugin(pluginName, handler); }).toThrow(`Plugin 'duplicate-plugin' is already registered`); }); test('should load plugins in registration order', () => { const handler1 = { middleware: jest.fn() }; const handler2 = { middleware: jest.fn() }; indexModule.registerPlugin('order-test-1', handler1); indexModule.registerPlugin('order-test-2', handler2); const handlers = indexModule.loadPlugins(); const registeredNames = indexModule.getRegisteredPluginNames(); // Check that our test plugins are in the registry expect(registeredNames).toContain('order-test-1'); expect(registeredNames).toContain('order-test-2'); // Check that our handlers are in the handlers array expect(handlers).toContain(handler1); expect(handlers).toContain(handler2); // Find the indices of our test handlers const handler1Index = handlers.indexOf(handler1); const handler2Index = handlers.indexOf(handler2); // Verify order (handler1 should come before handler2) expect(handler1Index).toBeLessThan(handler2Index); }); test('should freeze plugin registry', () => { const handler = { middleware: jest.fn() }; indexModule.registerPlugin('before-freeze', handler); indexModule.freezePlugins(); expect(mockLogs.msg).toHaveBeenCalledWith('Plugin registration frozen'); }); }); describe('Configuration Loading', () => { test('should load config successfully', async () => { const configName = 'test-config'; const target = {}; const configData = { setting1: 'value1' }; mockReadFile.mockResolvedValue('setting1 = "value1"'); mockJoin.mockReturnValue('/app/src/config/test-config.toml'); // Mock the TOML parser const mockTomlModule = { default: { parse: jest.fn(() => configData) } }; jest.unstable_mockModule('@iarna/toml', () => mockTomlModule); await indexModule.loadConfig(configName, target); expect(mockReadFile).toHaveBeenCalledWith('/app/src/config/test-config.toml', 'utf8'); expect(target).toEqual(configData); }); test('should throw error for invalid config name', async () => { await expect(indexModule.loadConfig('', {})).rejects.toThrow('Config name must be a non-empty string'); await expect(indexModule.loadConfig(null, {})).rejects.toThrow('Config name must be a non-empty string'); }); test('should throw error for invalid target', async () => { await expect(indexModule.loadConfig('test', null)).rejects.toThrow('Config target must be an object'); await expect(indexModule.loadConfig('test', 'invalid')).rejects.toThrow('Config target must be an object'); }); test('should handle config loading errors', async () => { const configName = 'failing-config'; const target = {}; mockReadFile.mockRejectedValue(new Error('File not found')); await expect(indexModule.loadConfig(configName, target)).rejects.toThrow( "Failed to load config 'failing-config': File not found" ); }); }); describe('Command Line Argument Handling', () => { test('should validate PID file operations for kill mode', () => { // Test the logic for handling kill mode operations mockExistsSync.mockReturnValue(true); mockReadFileSync.mockReturnValue('1234'); mockJoin.mockReturnValue('/app/src/checkpoint.pid'); const pidContent = mockReadFileSync('checkpoint.pid', 'utf8').trim(); const pid = parseInt(pidContent, 10); expect(pid).toBe(1234); expect(isNaN(pid)).toBe(false); expect(pid > 0).toBe(true); }); test('should validate invalid PID handling', () => { mockReadFileSync.mockReturnValue('invalid'); const pidContent = mockReadFileSync('checkpoint.pid', 'utf8').trim(); const pid = parseInt(pidContent, 10); expect(isNaN(pid)).toBe(true); }); test('should validate daemon spawn parameters', () => { const mockChildProcess = { pid: 5678, unref: jest.fn() }; mockSpawn.mockReturnValue(mockChildProcess); const args = ['index.js']; const nodeExecutable = 'node'; mockSpawn(nodeExecutable, args, { detached: true, stdio: 'ignore' }); expect(mockSpawn).toHaveBeenCalledWith( 'node', ['index.js'], expect.objectContaining({ detached: true, stdio: 'ignore' }) ); }); }); describe('Directory Initialization', () => { test('should validate directory creation logic', async () => { mockMkdir.mockResolvedValue(undefined); const expectedDirs = [ '/app/src/data', '/app/src/db', '/app/src/config' ]; // Test the directory creation logic for (const dirPath of expectedDirs) { mockJoin.mockReturnValueOnce(dirPath); await mockMkdir(dirPath, { recursive: true }); } expect(mockMkdir).toHaveBeenCalledTimes(3); expect(mockMkdir).toHaveBeenCalledWith(expect.any(String), { recursive: true }); }); }); describe('Plugin Discovery', () => { test('should discover plugins in correct order', () => { mockExistsSync.mockImplementation(path => { return path.includes('plugins'); }); mockReaddirSync.mockReturnValue([ 'waf.js', 'basic-auth.js', 'ipfilter.js', 'proxy.js', 'rate-limit.js' ]); mockBasename.mockImplementation((file, ext) => file.replace(ext || '', '') ); // Plugin load order should be: ipfilter, waf, proxy, then alphabetical const expectedOrder = ['ipfilter', 'waf', 'proxy', 'basic-auth', 'rate-limit']; expect(mockReaddirSync).toHaveBeenCalled; }); }); describe('Express Server Setup', () => { test('should validate Express app creation', () => { // Test Express app creation const app = mockExpress(); expect(mockExpress).toHaveBeenCalled(); expect(app).toBeDefined(); expect(app.set).toBeDefined(); expect(app.use).toBeDefined(); }); test('should validate middleware configuration', () => { // Test middleware setup logic const app = mockExpress(); // Test trust proxy setting app.set('trust proxy', true); expect(app.set).toHaveBeenCalledWith('trust proxy', true); // Test body parsing middleware const jsonMiddleware = mockExpress.json({ limit: '10mb' }); const urlencodedMiddleware = mockExpress.urlencoded({ extended: true, limit: '10mb' }); expect(mockExpress.json).toHaveBeenCalledWith({ limit: '10mb' }); expect(mockExpress.urlencoded).toHaveBeenCalledWith({ extended: true, limit: '10mb' }); }); test('should handle WebSocket upgrade detection logic', () => { const mockReq = { headers: { upgrade: 'websocket', connection: 'Upgrade' } }; const mockRes = {}; const mockNext = jest.fn(); // Test WebSocket detection logic const upgradeHeader = mockReq.headers.upgrade; const connectionHeader = mockReq.headers.connection; const isWebSocket = upgradeHeader === 'websocket' || (connectionHeader && connectionHeader.toLowerCase().includes('upgrade')); if (isWebSocket) { mockReq.isWebSocketRequest = true; mockNext(); } expect(mockReq.isWebSocketRequest).toBe(true); expect(mockNext).toHaveBeenCalled(); }); }); describe('Environment Configuration', () => { test('should handle production environment', () => { process.env.NODE_ENV = 'production'; // In production, console.log should be disabled // This is handled at module load time expect(process.env.NODE_ENV).toBe('production'); }); test('should handle custom port configuration', () => { process.env.PORT = '8080'; // Port validation would happen during server startup expect(process.env.PORT).toBe('8080'); }); test('should use default port when not specified', () => { delete process.env.PORT; // Default port should be 3000 const expectedPort = 3000; expect(expectedPort).toBe(3000); }); }); describe('Error Handling', () => { test('should validate plugin loading error handling', async () => { const error = new Error('Plugin load failed'); mockSecureImportModule.mockRejectedValue(error); // Test error handling logic try { await mockSecureImportModule('test-plugin.js'); } catch (e) { expect(e.message).toBe('Plugin load failed'); } expect(mockSecureImportModule).toHaveBeenCalled(); }); test('should validate config loading error handling', async () => { const error = new Error('Config read failed'); mockReadFile.mockRejectedValue(error); // Test config loading error handling try { await indexModule.loadConfig('test-config', {}); } catch (e) { expect(e.message).toContain('Failed to load config'); expect(e.message).toContain('Config read failed'); } }); test('should validate port number range', () => { const invalidPorts = [-1, 0, 65536, 70000]; const validPorts = [80, 443, 3000, 8080, 65535]; invalidPorts.forEach(port => { expect(isNaN(Number(port)) || Number(port) < 1 || Number(port) > 65535).toBe(true); }); validPorts.forEach(port => { expect(Number(port) >= 1 && Number(port) <= 65535).toBe(true); }); }); }); describe('Exclusion Rules Processing', () => { test('should compile exclusion patterns correctly', () => { const exclusionRules = [ { Path: '/api/health', Hosts: ['localhost', '127.0.0.1'], UserAgents: ['healthcheck.*'] } ]; // Test exclusion rule compilation logic const compiledRule = { ...exclusionRules[0], pathStartsWith: exclusionRules[0].Path, hostsSet: new Set(exclusionRules[0].Hosts), userAgentPatterns: exclusionRules[0].UserAgents.map(pattern => new RegExp(pattern, 'i')) }; expect(compiledRule.hostsSet.has('localhost')).toBe(true); expect(compiledRule.userAgentPatterns[0].test('healthcheck-bot')).toBe(true); }); test('should handle invalid regex patterns in UserAgents', () => { const invalidPattern = '[invalid regex'; try { new RegExp(invalidPattern, 'i'); } catch (e) { // Should handle invalid regex gracefully const fallbackPattern = /(?!)/; // Never matches expect(fallbackPattern.test('anything')).toBe(false); } }); }); describe('Static File Middleware', () => { test('should validate static file setup for existing directories', () => { const webfontPath = '/app/src/pages/interstitial/webfont'; const jsPath = '/app/src/pages/interstitial/js'; mockExistsSync.mockImplementation(path => { return path === webfontPath || path === jsPath; }); const router = mockExpress.Router(); // Test static directory setup logic if (mockExistsSync(webfontPath)) { router.use('/webfont', mockExpress.static(webfontPath, { maxAge: '7d' })); } if (mockExistsSync(jsPath)) { router.use('/js', mockExpress.static(jsPath, { maxAge: '7d' })); } expect(mockExistsSync).toHaveBeenCalledWith(webfontPath); expect(mockExistsSync).toHaveBeenCalledWith(jsPath); expect(mockExpress.static).toHaveBeenCalledWith(webfontPath, { maxAge: '7d' }); expect(mockExpress.static).toHaveBeenCalledWith(jsPath, { maxAge: '7d' }); }); test('should skip static setup for non-existent directories', () => { mockExistsSync.mockReturnValue(false); const webfontPath = '/app/src/pages/interstitial/webfont'; const jsPath = '/app/src/pages/interstitial/js'; // Test logic when directories don't exist const webfontExists = mockExistsSync(webfontPath); const jsExists = mockExistsSync(jsPath); expect(webfontExists).toBe(false); expect(jsExists).toBe(false); expect(mockExistsSync).toHaveBeenCalledTimes(2); }); }); describe('Graceful Shutdown', () => { test('should validate shutdown handler logic', () => { const mockShutdownHandler = jest.fn(); const activeSockets = new Set(); const mockSocketInstance = { destroy: jest.fn(), setTimeout: jest.fn(), setKeepAlive: jest.fn(), on: jest.fn() }; activeSockets.add(mockSocketInstance); // Test shutdown logic const isShuttingDown = false; if (!isShuttingDown) { activeSockets.forEach(sock => sock.destroy()); mockShutdownHandler(); } expect(mockSocketInstance.destroy).toHaveBeenCalled(); expect(mockShutdownHandler).toHaveBeenCalled(); }); test('should validate socket management', () => { const activeSockets = new Set(); const mockSocket = { destroy: jest.fn(), setTimeout: jest.fn(), setKeepAlive: jest.fn(), on: jest.fn() }; // Test socket lifecycle activeSockets.add(mockSocket); expect(activeSockets.has(mockSocket)).toBe(true); // Test socket configuration mockSocket.setTimeout(120000); mockSocket.setKeepAlive(true, 60000); expect(mockSocket.setTimeout).toHaveBeenCalledWith(120000); expect(mockSocket.setKeepAlive).toHaveBeenCalledWith(true, 60000); // Test cleanup activeSockets.delete(mockSocket); expect(activeSockets.has(mockSocket)).toBe(false); }); }); });