import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import path from 'path'; describe('security.ts', () => { let originalEnv: NodeJS.ProcessEnv; beforeEach(() => { // Save original environment originalEnv = { ...process.env }; // Reset modules to get fresh state vi.resetModules(); }); afterEach(() => { // Restore original environment process.env = originalEnv; }); describe('initAllowedPaths', () => { it('should load ALLOWED_ROOT_DIRECTORY if set', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/projects'; delete process.env.DATA_DIR; const { initAllowedPaths, getAllowedPaths } = await import('../src/security'); initAllowedPaths(); const allowed = getAllowedPaths(); expect(allowed).toContain(path.resolve('/projects')); }); it('should load DATA_DIR if set', async () => { delete process.env.ALLOWED_ROOT_DIRECTORY; process.env.DATA_DIR = '/data/directory'; const { initAllowedPaths, getAllowedPaths } = await import('../src/security'); initAllowedPaths(); const allowed = getAllowedPaths(); expect(allowed).toContain(path.resolve('/data/directory')); }); it('should load both ALLOWED_ROOT_DIRECTORY and DATA_DIR if both set', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/projects'; process.env.DATA_DIR = '/app/data'; const { initAllowedPaths, getAllowedPaths } = await import('../src/security'); initAllowedPaths(); const allowed = getAllowedPaths(); expect(allowed).toContain(path.resolve('/projects')); expect(allowed).toContain(path.resolve('/app/data')); }); it('should handle missing environment variables gracefully', async () => { delete process.env.ALLOWED_ROOT_DIRECTORY; delete process.env.DATA_DIR; const { initAllowedPaths } = await import('../src/security'); expect(() => initAllowedPaths()).not.toThrow(); }); }); describe('isPathAllowed', () => { it('should allow paths within ALLOWED_ROOT_DIRECTORY', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/allowed'; delete process.env.DATA_DIR; const { initAllowedPaths, isPathAllowed } = await import('../src/security'); initAllowedPaths(); expect(isPathAllowed('/allowed/file.txt')).toBe(true); expect(isPathAllowed('/allowed/subdir/file.txt')).toBe(true); }); it('should deny paths outside ALLOWED_ROOT_DIRECTORY', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/allowed'; delete process.env.DATA_DIR; const { initAllowedPaths, isPathAllowed } = await import('../src/security'); initAllowedPaths(); expect(isPathAllowed('/not-allowed/file.txt')).toBe(false); expect(isPathAllowed('/etc/passwd')).toBe(false); }); it('should always allow DATA_DIR paths', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/projects'; process.env.DATA_DIR = '/app/data'; const { initAllowedPaths, isPathAllowed } = await import('../src/security'); initAllowedPaths(); // DATA_DIR paths are always allowed expect(isPathAllowed('/app/data/settings.json')).toBe(true); expect(isPathAllowed('/app/data/credentials.json')).toBe(true); }); it('should allow all paths when no restrictions configured', async () => { delete process.env.ALLOWED_ROOT_DIRECTORY; delete process.env.DATA_DIR; const { initAllowedPaths, isPathAllowed } = await import('../src/security'); initAllowedPaths(); expect(isPathAllowed('/any/path')).toBe(true); expect(isPathAllowed('/etc/passwd')).toBe(true); }); it('should allow all paths when only DATA_DIR is configured', async () => { delete process.env.ALLOWED_ROOT_DIRECTORY; process.env.DATA_DIR = '/data'; const { initAllowedPaths, isPathAllowed } = await import('../src/security'); initAllowedPaths(); // DATA_DIR should be allowed expect(isPathAllowed('/data/file.txt')).toBe(true); // And all other paths should be allowed since no ALLOWED_ROOT_DIRECTORY restriction expect(isPathAllowed('/any/path')).toBe(true); }); }); describe('validatePath', () => { it('should return resolved path for allowed paths', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/allowed'; delete process.env.DATA_DIR; const { initAllowedPaths, validatePath } = await import('../src/security'); initAllowedPaths(); const result = validatePath('/allowed/file.txt'); expect(result).toBe(path.resolve('/allowed/file.txt')); }); it('should throw error for paths outside allowed directories', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/allowed'; delete process.env.DATA_DIR; const { initAllowedPaths, validatePath, PathNotAllowedError } = await import('../src/security'); initAllowedPaths(); expect(() => validatePath('/not-allowed/file.txt')).toThrow(PathNotAllowedError); }); it('should resolve relative paths', async () => { const cwd = process.cwd(); process.env.ALLOWED_ROOT_DIRECTORY = cwd; delete process.env.DATA_DIR; const { initAllowedPaths, validatePath } = await import('../src/security'); initAllowedPaths(); const result = validatePath('./file.txt'); expect(result).toBe(path.resolve(cwd, './file.txt')); }); it('should not throw when no restrictions configured', async () => { delete process.env.ALLOWED_ROOT_DIRECTORY; delete process.env.DATA_DIR; const { initAllowedPaths, validatePath } = await import('../src/security'); initAllowedPaths(); expect(() => validatePath('/any/path')).not.toThrow(); }); }); describe('getAllowedPaths', () => { it('should return empty array when no paths configured', async () => { delete process.env.ALLOWED_ROOT_DIRECTORY; delete process.env.DATA_DIR; const { initAllowedPaths, getAllowedPaths } = await import('../src/security'); initAllowedPaths(); const allowed = getAllowedPaths(); expect(Array.isArray(allowed)).toBe(true); expect(allowed).toHaveLength(0); }); it('should return configured paths', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/projects'; process.env.DATA_DIR = '/data'; const { initAllowedPaths, getAllowedPaths } = await import('../src/security'); initAllowedPaths(); const allowed = getAllowedPaths(); expect(allowed).toContain(path.resolve('/projects')); expect(allowed).toContain(path.resolve('/data')); }); }); describe('getAllowedRootDirectory', () => { it('should return the configured root directory', async () => { process.env.ALLOWED_ROOT_DIRECTORY = '/projects'; const { initAllowedPaths, getAllowedRootDirectory } = await import('../src/security'); initAllowedPaths(); expect(getAllowedRootDirectory()).toBe(path.resolve('/projects')); }); it('should return null when not configured', async () => { delete process.env.ALLOWED_ROOT_DIRECTORY; const { initAllowedPaths, getAllowedRootDirectory } = await import('../src/security'); initAllowedPaths(); expect(getAllowedRootDirectory()).toBeNull(); }); }); describe('getDataDirectory', () => { it('should return the configured data directory', async () => { process.env.DATA_DIR = '/data'; const { initAllowedPaths, getDataDirectory } = await import('../src/security'); initAllowedPaths(); expect(getDataDirectory()).toBe(path.resolve('/data')); }); it('should return null when not configured', async () => { delete process.env.DATA_DIR; const { initAllowedPaths, getDataDirectory } = await import('../src/security'); initAllowedPaths(); expect(getDataDirectory()).toBeNull(); }); }); });