import { jest } from '@jest/globals'; // Mock the path and url modules for testing jest.unstable_mockModule('path', () => ({ resolve: jest.fn(), extname: jest.fn(), sep: '/', isAbsolute: jest.fn(), normalize: jest.fn() })); jest.unstable_mockModule('url', () => ({ pathToFileURL: jest.fn() })); // Mock the root directory jest.unstable_mockModule('../dist/index.js', () => ({ rootDir: '/app/root' })); // Import after mocking const { secureImportModule, hasExport, getExport } = await import('../dist/utils/plugins.js'); const path = await import('path'); const url = await import('url'); describe('Plugins utilities', () => { beforeEach(() => { jest.clearAllMocks(); // Default mock implementations path.normalize.mockImplementation((p) => p); path.resolve.mockImplementation((root, rel) => `${root}/${rel}`); path.extname.mockImplementation((p) => { const parts = p.split('.'); return parts.length > 1 ? `.${parts[parts.length - 1]}` : ''; }); path.isAbsolute.mockImplementation((p) => p.startsWith('/')); url.pathToFileURL.mockImplementation((p) => ({ href: `file://${p}` })); }); describe('secureImportModule', () => { describe('input validation', () => { test('should reject non-string module paths', async () => { await expect(secureImportModule(null)).rejects.toThrow('Module path must be a string'); await expect(secureImportModule(undefined)).rejects.toThrow('Module path must be a string'); await expect(secureImportModule(123)).rejects.toThrow('Module path must be a string'); }); test('should reject empty module paths', async () => { await expect(secureImportModule('')).rejects.toThrow('Module path cannot be empty'); }); test('should reject paths that are too long', async () => { const longPath = 'a'.repeat(1025) + '.js'; await expect(secureImportModule(longPath)).rejects.toThrow('Module path too long'); }); }); describe('security pattern validation', () => { test('should reject directory traversal attempts', async () => { await expect(secureImportModule('../evil.js')).rejects.toThrow('Module path contains blocked pattern'); await expect(secureImportModule('../../system.js')).rejects.toThrow('Module path contains blocked pattern'); await expect(secureImportModule('folder/../escape.js')).rejects.toThrow('Module path contains blocked pattern'); }); test('should reject double slash patterns', async () => { await expect(secureImportModule('folder//file.js')).rejects.toThrow('Module path contains blocked pattern'); await expect(secureImportModule('//root.js')).rejects.toThrow('Module path contains blocked pattern'); }); test('should reject null byte injections', async () => { await expect(secureImportModule('file\0.js')).rejects.toThrow('Module path contains blocked pattern'); }); test('should reject control characters', async () => { await expect(secureImportModule('file\x01.js')).rejects.toThrow('Module path contains blocked pattern'); await expect(secureImportModule('file\x1f.js')).rejects.toThrow('Module path contains blocked pattern'); }); test('should reject node_modules access', async () => { await expect(secureImportModule('node_modules/evil.js')).rejects.toThrow('Module path contains blocked pattern'); await expect(secureImportModule('NODE_MODULES/evil.js')).rejects.toThrow('Module path contains blocked pattern'); }); test('should reject sensitive file access', async () => { await expect(secureImportModule('package.json')).rejects.toThrow('Module path contains blocked pattern'); await expect(secureImportModule('.env')).rejects.toThrow('Module path contains blocked pattern'); await expect(secureImportModule('config/.ENV')).rejects.toThrow('Module path contains blocked pattern'); }); test('should reject paths that are too deep', async () => { const deepPath = 'a/'.repeat(25) + 'file.js'; path.normalize.mockReturnValue(deepPath); await expect(secureImportModule(deepPath)).rejects.toThrow('Module path too deep'); }); }); describe('file extension validation', () => { test('should reject invalid file extensions', async () => { path.extname.mockReturnValue('.txt'); await expect(secureImportModule('file.txt')).rejects.toThrow('Only .js, .mjs files can be imported'); }); test('should reject files without extensions', async () => { path.extname.mockReturnValue(''); await expect(secureImportModule('file')).rejects.toThrow('Only .js, .mjs files can be imported'); }); test('should accept valid .js extension validation', async () => { path.extname.mockReturnValue('.js'); path.isAbsolute.mockReturnValue(false); path.resolve.mockReturnValue('/app/root/valid.js'); path.normalize.mockImplementation((p) => p); // This will fail at import but pass extension validation await expect(secureImportModule('valid.js')).rejects.toThrow('Failed to import module'); }); test('should accept valid .mjs extension validation', async () => { path.extname.mockReturnValue('.mjs'); path.isAbsolute.mockReturnValue(false); path.resolve.mockReturnValue('/app/root/valid.mjs'); path.normalize.mockImplementation((p) => p); // This will fail at import but pass extension validation await expect(secureImportModule('valid.mjs')).rejects.toThrow('Failed to import module'); }); }); describe('path resolution security', () => { test('should reject absolute paths', async () => { path.extname.mockReturnValue('.js'); path.isAbsolute.mockReturnValue(true); await expect(secureImportModule('/absolute/path.js')).rejects.toThrow('Absolute paths are not allowed'); }); test('should reject paths outside application root', async () => { path.extname.mockReturnValue('.js'); path.isAbsolute.mockReturnValue(false); path.resolve.mockReturnValue('/outside/root/file.js'); path.normalize.mockImplementation((p) => p); await expect(secureImportModule('file.js')).rejects.toThrow('Module path outside of application root'); }); test('should detect symbolic link traversal', async () => { path.extname.mockReturnValue('.js'); path.isAbsolute.mockReturnValue(false); path.resolve.mockReturnValue('/app/root/../outside.js'); path.normalize.mockImplementation((p) => p); await expect(secureImportModule('file.js')).rejects.toThrow('Path traversal detected'); }); test('should validate paths within application root', async () => { path.extname.mockReturnValue('.js'); path.isAbsolute.mockReturnValue(false); path.resolve.mockReturnValue('/app/root/plugins/test.js'); path.normalize.mockImplementation((p) => p); // This will fail at import but pass path validation await expect(secureImportModule('plugins/test.js')).rejects.toThrow('Failed to import module'); }); }); describe('import operation and error handling', () => { test('should handle import failures gracefully', async () => { path.extname.mockReturnValue('.js'); path.isAbsolute.mockReturnValue(false); path.resolve.mockReturnValue('/app/root/file.js'); path.normalize.mockImplementation((p) => p); // Since we can't easily mock import() in ES modules, // this will naturally fail and test our error handling await expect(secureImportModule('file.js')).rejects.toThrow('Failed to import module'); }); test('should handle non-Error exceptions in path validation', async () => { path.extname.mockReturnValue('.js'); path.isAbsolute.mockReturnValue(false); path.resolve.mockImplementation(() => { throw "Non-error exception"; }); await expect(secureImportModule('file.js')).rejects.toThrow('Module import failed'); }); test('should handle unknown errors gracefully', async () => { path.extname.mockImplementation(() => { throw { unknown: 'error' }; }); await expect(secureImportModule('file.js')).rejects.toThrow('Module import failed due to unknown error'); }); }); }); describe('hasExport', () => { test('should return true for existing exports', () => { const module = { testFunction: () => {}, testValue: 'test' }; expect(hasExport(module, 'testFunction')).toBe(true); expect(hasExport(module, 'testValue')).toBe(true); }); test('should return false for non-existent exports', () => { const module = { testFunction: () => {} }; expect(hasExport(module, 'nonExistent')).toBe(false); }); test('should return false for undefined exports', () => { const module = { testValue: undefined }; expect(hasExport(module, 'testValue')).toBe(false); }); test('should handle edge cases', () => { const module = {}; expect(hasExport(module, 'toString')).toBe(true); // inherited from Object.prototype expect(hasExport(module, 'constructor')).toBe(true); // inherited from Object.prototype expect(hasExport(module, 'nonExistentMethod')).toBe(false); }); }); describe('getExport', () => { test('should return export values correctly', () => { const testFunction = () => 'test'; const module = { testFunction, testValue: 'hello' }; expect(getExport(module, 'testFunction')).toBe(testFunction); expect(getExport(module, 'testValue')).toBe('hello'); }); test('should return undefined for non-existent exports', () => { const module = { testFunction: () => {} }; expect(getExport(module, 'nonExistent')).toBeUndefined(); }); test('should return actual values including falsy ones', () => { const module = { zero: 0, false: false, empty: '', null: null }; expect(getExport(module, 'zero')).toBe(0); expect(getExport(module, 'false')).toBe(false); expect(getExport(module, 'empty')).toBe(''); expect(getExport(module, 'null')).toBe(null); }); }); });