path-access-control.test.tsā¢13 kB
import { PathAccessControl } from '../path-access-control.js';
import { existsSync, mkdirSync, writeFileSync, unlinkSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
describe('PathAccessControl', () => {
let testDir: string;
let allowFilePath: string;
beforeEach(() => {
// Create a unique test directory for each test
testDir = join(tmpdir(), `path-access-control-test-${Date.now()}-${Math.random()}`);
mkdirSync(testDir, { recursive: true });
allowFilePath = join(testDir, 'paths.allow');
});
afterEach(() => {
// Clean up test directory
if (existsSync(testDir)) {
rmSync(testDir, { recursive: true, force: true });
}
});
describe('Default behavior (file does not exist)', () => {
it('should allow all paths when allowlist file does not exist', () => {
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/any/path/file.txt')).toBe(true);
expect(control.isPathAllowed('/home/user/project/src/index.ts')).toBe(true);
expect(control.isPathAllowed('/tmp/test.js')).toBe(true);
});
it('should default to wildcard pattern when file does not exist', () => {
const control = new PathAccessControl(allowFilePath);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(1);
expect(patterns[0].pattern).toBe('**/*');
expect(patterns[0].isNegation).toBe(false);
});
});
describe('Simple patterns', () => {
it('should allow paths matching a simple glob pattern', () => {
writeFileSync(allowFilePath, '/home/user/project/**\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project/file.txt')).toBe(true);
expect(control.isPathAllowed('/home/user/project/src/index.ts')).toBe(true);
expect(control.isPathAllowed('/home/other/file.txt')).toBe(false);
});
it('should handle multiple allow patterns', () => {
writeFileSync(
allowFilePath,
'/home/user/project1/**\n/home/user/project2/**\n/home/user/project3/**\n'
);
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project1/file.txt')).toBe(true);
expect(control.isPathAllowed('/home/user/project2/file.txt')).toBe(true);
expect(control.isPathAllowed('/home/user/project3/file.txt')).toBe(true);
expect(control.isPathAllowed('/home/user/project4/file.txt')).toBe(false);
});
it('should handle wildcard pattern', () => {
writeFileSync(allowFilePath, '*\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/any/path/file.txt')).toBe(true);
expect(control.isPathAllowed('/home/user/project/src/index.ts')).toBe(true);
});
});
describe('Negation patterns', () => {
it('should deny paths matching negation pattern', () => {
writeFileSync(allowFilePath, '/home/user/project/**\n!**/node_modules/**\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project/src/index.ts')).toBe(true);
expect(control.isPathAllowed('/home/user/project/node_modules/lib/file.js')).toBe(false);
expect(control.isPathAllowed('/home/user/project/src/node_modules/test.js')).toBe(false);
});
it('should handle multiple negation patterns', () => {
writeFileSync(
allowFilePath,
'/home/user/project/**\n!**/node_modules/**\n!**/.git/**\n!**/dist/**\n'
);
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project/src/index.ts')).toBe(true);
expect(control.isPathAllowed('/home/user/project/node_modules/lib.js')).toBe(false);
expect(control.isPathAllowed('/home/user/project/.git/config')).toBe(false);
expect(control.isPathAllowed('/home/user/project/dist/bundle.js')).toBe(false);
});
it('should evaluate patterns in order (last match wins)', () => {
// First allow all, then deny node_modules, then re-allow specific node_modules
writeFileSync(
allowFilePath,
'/home/user/project/**\n!**/node_modules/**\n/home/user/project/node_modules/special/**\n'
);
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project/src/index.ts')).toBe(true);
expect(control.isPathAllowed('/home/user/project/node_modules/lib.js')).toBe(false);
expect(control.isPathAllowed('/home/user/project/node_modules/special/file.js')).toBe(true);
});
});
describe('Relative patterns', () => {
it('should handle relative patterns by converting to **/ prefix', () => {
writeFileSync(allowFilePath, '*.ts\n*.js\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project/file.ts')).toBe(true);
expect(control.isPathAllowed('/home/user/project/file.js')).toBe(true);
expect(control.isPathAllowed('/home/user/project/file.py')).toBe(false);
});
it('should handle patterns already starting with **/', () => {
writeFileSync(allowFilePath, '**/*.ts\n');
const control = new PathAccessControl(allowFilePath);
const patterns = control.getPatterns();
expect(patterns[0].pattern).toBe('**/*.ts');
expect(control.isPathAllowed('/home/user/project/file.ts')).toBe(true);
expect(control.isPathAllowed('/home/user/project/src/index.ts')).toBe(true);
expect(control.isPathAllowed('/home/user/project/file.js')).toBe(false);
});
});
describe('Comments and empty lines', () => {
it('should ignore comment lines', () => {
writeFileSync(
allowFilePath,
'# This is a comment\n/home/user/project/**\n# Another comment\n'
);
const control = new PathAccessControl(allowFilePath);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(1);
expect(patterns[0].pattern).toBe('/home/user/project/**');
});
it('should ignore empty lines', () => {
writeFileSync(allowFilePath, '/home/user/project/**\n\n\n/home/user/other/**\n');
const control = new PathAccessControl(allowFilePath);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(2);
});
it('should handle mixed comments, empty lines, and patterns', () => {
writeFileSync(
allowFilePath,
`# Allow project directory
/home/user/project/**
# Deny node_modules
!**/node_modules/**
# Deny .git
!**/.git/**
`
);
const control = new PathAccessControl(allowFilePath);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(3);
expect(patterns[0].isNegation).toBe(false);
expect(patterns[1].isNegation).toBe(true);
expect(patterns[2].isNegation).toBe(true);
});
});
describe('Empty or invalid file', () => {
it('should default to allow all when file is empty', () => {
writeFileSync(allowFilePath, '');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/any/path/file.txt')).toBe(true);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(1);
expect(patterns[0].pattern).toBe('**/*');
});
it('should default to allow all when file contains only comments', () => {
writeFileSync(allowFilePath, '# Comment 1\n# Comment 2\n# Comment 3\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/any/path/file.txt')).toBe(true);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(1);
expect(patterns[0].pattern).toBe('**/*');
});
it('should ignore invalid negation-only lines', () => {
writeFileSync(allowFilePath, '!\n/home/user/project/**\n');
const control = new PathAccessControl(allowFilePath);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(1);
expect(patterns[0].pattern).toBe('/home/user/project/**');
});
});
describe('Reload functionality', () => {
it('should reload patterns from file', () => {
writeFileSync(allowFilePath, '/home/user/project1/**\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project1/file.txt')).toBe(true);
expect(control.isPathAllowed('/home/user/project2/file.txt')).toBe(false);
// Update file
writeFileSync(allowFilePath, '/home/user/project2/**\n');
control.reload();
expect(control.isPathAllowed('/home/user/project1/file.txt')).toBe(false);
expect(control.isPathAllowed('/home/user/project2/file.txt')).toBe(true);
});
it('should handle file deletion during reload', () => {
writeFileSync(allowFilePath, '/home/user/project/**\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project/file.txt')).toBe(true);
expect(control.isPathAllowed('/other/file.txt')).toBe(false);
// Delete file
unlinkSync(allowFilePath);
control.reload();
// Should default to allow all
expect(control.isPathAllowed('/home/user/project/file.txt')).toBe(true);
expect(control.isPathAllowed('/other/file.txt')).toBe(true);
});
});
describe('Complex scenarios', () => {
it('should handle real-world allowlist configuration', () => {
writeFileSync(
allowFilePath,
`/home/user/projects/**
!**/node_modules/**
!**/.git/**
!**/dist/**
!**/build/**
!**/.DS_Store
!**/coverage/**
`
);
const control = new PathAccessControl(allowFilePath);
// Allowed paths
expect(control.isPathAllowed('/home/user/projects/app/src/index.ts')).toBe(true);
expect(control.isPathAllowed('/home/user/projects/app/README.md')).toBe(true);
expect(control.isPathAllowed('/home/user/projects/lib/package.json')).toBe(true);
// Denied paths
expect(control.isPathAllowed('/home/user/projects/app/node_modules/lib.js')).toBe(false);
expect(control.isPathAllowed('/home/user/projects/app/.git/config')).toBe(false);
expect(control.isPathAllowed('/home/user/projects/app/dist/bundle.js')).toBe(false);
expect(control.isPathAllowed('/home/user/projects/app/build/output.js')).toBe(false);
expect(control.isPathAllowed('/home/user/projects/app/.DS_Store')).toBe(false);
expect(control.isPathAllowed('/home/user/projects/app/coverage/index.html')).toBe(false);
});
it('should handle workspace-restricted configuration', () => {
const workspace = '/home/user/my-current-project';
writeFileSync(allowFilePath, `${workspace}/**\n`);
const control = new PathAccessControl(allowFilePath);
// Only allow within workspace
expect(control.isPathAllowed(`${workspace}/src/index.ts`)).toBe(true);
expect(control.isPathAllowed(`${workspace}/deep/nested/file.js`)).toBe(true);
expect(control.isPathAllowed('/home/user/other-project/file.ts')).toBe(false);
expect(control.isPathAllowed('/etc/passwd')).toBe(false);
});
});
describe('Pattern information', () => {
it('should return pattern metadata', () => {
writeFileSync(allowFilePath, '/home/user/project/**\n!**/node_modules/**\n');
const control = new PathAccessControl(allowFilePath);
const patterns = control.getPatterns();
expect(patterns).toHaveLength(2);
expect(patterns[0]).toMatchObject({
pattern: '/home/user/project/**',
isNegation: false,
originalLine: '/home/user/project/**',
});
expect(patterns[1]).toMatchObject({
pattern: '**/node_modules/**',
isNegation: true,
originalLine: '!**/node_modules/**',
});
});
it('should return allowlist file path', () => {
const control = new PathAccessControl(allowFilePath);
expect(control.getAllowFilePath()).toBe(allowFilePath);
});
});
describe('Edge cases', () => {
it('should handle Windows-style paths on Unix systems', () => {
// This test ensures path resolution works correctly
writeFileSync(allowFilePath, '/home/user/project/**\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/project/src/file.ts')).toBe(true);
});
it('should handle paths with special characters', () => {
writeFileSync(allowFilePath, '/home/user/my-project/**\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed('/home/user/my-project/file.ts')).toBe(true);
});
it('should handle very long paths', () => {
const longPath = '/home/user/' + 'a/'.repeat(100) + 'file.ts';
writeFileSync(allowFilePath, '/home/user/**\n');
const control = new PathAccessControl(allowFilePath);
expect(control.isPathAllowed(longPath)).toBe(true);
});
});
});