Checkpoint/.tests/index.test.js
2025-08-02 15:34:04 -05:00

623 lines
No EOL
19 KiB
JavaScript

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);
});
});
});