| import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; |
| import path from 'path'; |
|
|
| describe('security.ts', () => { |
| let originalEnv: NodeJS.ProcessEnv; |
|
|
| beforeEach(() => { |
| |
| originalEnv = { ...process.env }; |
| |
| vi.resetModules(); |
| }); |
|
|
| afterEach(() => { |
| |
| 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(); |
|
|
| |
| 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(); |
|
|
| |
| expect(isPathAllowed('/data/file.txt')).toBe(true); |
| |
| 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(); |
| }); |
| }); |
| }); |
|
|