import { describe, expect, test } from 'bun:test'; import { shellEscape } from './shell'; describe('shellEscape', () => { describe('simple paths', () => { test('escapes path without special characters', () => { expect(shellEscape('/tmp/test')).toBe("'/tmp/test'"); }); test('escapes absolute path', () => { expect(shellEscape('/usr/local/bin/celilo')).toBe("'/usr/local/bin/celilo'"); }); test('escapes relative path', () => { expect(shellEscape('./modules/homebridge')).toBe("'./modules/homebridge'"); }); test('escapes path with numbers', () => { expect(shellEscape('/tmp/test123')).toBe("'/tmp/test123'"); }); }); describe('paths with spaces', () => { test('escapes path with single space', () => { expect(shellEscape('/tmp/test module')).toBe("'/tmp/test module'"); }); test('escapes path with multiple spaces', () => { expect(shellEscape('/tmp/test module build')).toBe("'/tmp/test module build'"); }); test('escapes macOS Application Support path', () => { expect(shellEscape('/Users/user/Library/Application Support/celilo')).toBe( "'/Users/user/Library/Application Support/celilo'", ); }); test('escapes path with leading space', () => { expect(shellEscape(' /tmp/test')).toBe("' /tmp/test'"); }); test('escapes path with trailing space', () => { expect(shellEscape('/tmp/test ')).toBe("'/tmp/test '"); }); }); describe('paths with quotes', () => { test('escapes path with single quote', () => { expect(shellEscape("/tmp/Bob's Files")).toBe("'/tmp/Bob'\\''s Files'"); }); test('escapes path with double quote', () => { expect(shellEscape('/tmp/"quoted"')).toBe('\'/tmp/"quoted"\''); }); test('escapes path with multiple single quotes', () => { expect(shellEscape("/tmp/Bob's Alice's Files")).toBe("'/tmp/Bob'\\''s Alice'\\''s Files'"); }); test('escapes path with mixed quotes', () => { expect(shellEscape('/tmp/Bob\'s "Files"')).toBe("'/tmp/Bob'\\''s \"Files\"'"); }); }); describe('paths with special characters', () => { test('escapes path with dollar sign', () => { expect(shellEscape('/tmp/$VAR')).toBe("'/tmp/$VAR'"); }); test('escapes path with backtick', () => { expect(shellEscape('/tmp/`cmd`')).toBe("'/tmp/`cmd`'"); }); test('escapes path with exclamation mark', () => { expect(shellEscape('/tmp/test!')).toBe("'/tmp/test!'"); }); test('escapes path with ampersand', () => { expect(shellEscape('/tmp/test&bg')).toBe("'/tmp/test&bg'"); }); test('escapes path with pipe', () => { expect(shellEscape('/tmp/test|pipe')).toBe("'/tmp/test|pipe'"); }); test('escapes path with semicolon', () => { expect(shellEscape('/tmp/test;cmd')).toBe("'/tmp/test;cmd'"); }); test('escapes path with angle brackets', () => { expect(shellEscape('/tmp/')).toBe("'/tmp/'"); }); test('escapes path with parentheses', () => { expect(shellEscape('/tmp/(group)')).toBe("'/tmp/(group)'"); }); test('escapes path with brackets', () => { expect(shellEscape('/tmp/[test]')).toBe("'/tmp/[test]'"); }); test('escapes path with braces', () => { expect(shellEscape('/tmp/{a,b}')).toBe("'/tmp/{a,b}'"); }); test('escapes path with asterisk', () => { expect(shellEscape('/tmp/*')).toBe("'/tmp/*'"); }); test('escapes path with question mark', () => { expect(shellEscape('/tmp/test?')).toBe("'/tmp/test?'"); }); test('escapes path with tilde', () => { expect(shellEscape('~/Documents')).toBe("'~/Documents'"); }); test('escapes path with hash', () => { expect(shellEscape('/tmp/#comment')).toBe("'/tmp/#comment'"); }); test('escapes path with backslash', () => { expect(shellEscape('/tmp\\test')).toBe("'/tmp\\test'"); }); }); describe('edge cases', () => { test('throws on empty string', () => { expect(() => shellEscape('')).toThrow('Cannot escape empty or null path'); }); test('escapes path with only spaces', () => { expect(shellEscape(' ')).toBe("' '"); }); test('escapes path with newline', () => { expect(shellEscape('/tmp/test\nline')).toBe("'/tmp/test\nline'"); }); test('escapes path with tab', () => { expect(shellEscape('/tmp/test\tfile')).toBe("'/tmp/test\tfile'"); }); test('escapes unicode characters', () => { expect(shellEscape('/tmp/test™')).toBe("'/tmp/test™'"); }); test('escapes emoji in path', () => { expect(shellEscape('/tmp/test-😀-file')).toBe("'/tmp/test-😀-file'"); }); }); describe('real-world examples', () => { test('escapes typical module path with spaces', () => { const path = '/Users/user/Library/Application Support/celilo/modules/test module'; expect(shellEscape(path)).toBe( "'/Users/user/Library/Application Support/celilo/modules/test module'", ); }); test('escapes build directory with spaces', () => { const path = '/tmp/celilo modules/caddy/build'; expect(shellEscape(path)).toBe("'/tmp/celilo modules/caddy/build'"); }); test('escapes Windows-style path (if encountered)', () => { const path = 'C:\\Program Files\\Celilo'; expect(shellEscape(path)).toBe("'C:\\Program Files\\Celilo'"); }); }); }); describe('usage examples', () => { test('example: cd command with spaces', () => { const modulePath = '/Users/user/Library/Application Support/celilo'; const command = `cd ${shellEscape(modulePath)} && ls`; expect(command).toBe("cd '/Users/user/Library/Application Support/celilo' && ls"); }); test('example: cp command with multiple paths', () => { const source = '/tmp/test module'; const dest = '/dest/final location'; const command = `cp ${shellEscape(source)} ${shellEscape(dest)}`; expect(command).toBe("cp '/tmp/test module' '/dest/final location'"); }); test('example: tar command with quoted filename', () => { const filename = 'backup-2024.tar.gz'; const directory = '/Users/user/My Documents'; const command = `tar -czf ${shellEscape(filename)} -C ${shellEscape(directory)} .`; expect(command).toBe("tar -czf 'backup-2024.tar.gz' -C '/Users/user/My Documents' ."); }); test('example: handling path with single quote', () => { const path = "/Users/Bob's MacBook/Documents"; const command = `ls ${shellEscape(path)}`; expect(command).toBe("ls '/Users/Bob'\\''s MacBook/Documents'"); }); });