|
1 | | -import path, { PlatformPath } from 'path'; |
2 | | -import { splitHubSpotPath, splitLocalPath } from '../path'; |
| 1 | +import os from 'os'; |
| 2 | +import { |
| 3 | + convertToUnixPath, |
| 4 | + splitLocalPath, |
| 5 | + splitHubSpotPath, |
| 6 | + getCwd, |
| 7 | + getExt, |
| 8 | + getAllowedExtensions, |
| 9 | + isAllowedExtension, |
| 10 | + sanitizeFileName, |
| 11 | + isValidPath, |
| 12 | + untildify, |
| 13 | +} from '../path'; |
| 14 | +import { ALLOWED_EXTENSIONS } from '../../constants/extensions'; |
3 | 15 |
|
4 | | -describe('lib/path', () => { |
5 | | - describe('splitHubSpotPath()', () => { |
6 | | - const testSplit = ( |
7 | | - filepath: string, |
8 | | - expectedParts: Array<string>, |
9 | | - joined: string |
10 | | - ) => { |
11 | | - test(filepath, () => { |
12 | | - const parts = splitHubSpotPath(filepath); |
13 | | - expect(parts).toEqual(expectedParts); |
14 | | - expect(path.posix.join(...parts)).toBe(joined); |
15 | | - }); |
16 | | - }; |
17 | | - testSplit('', [], '.'); |
18 | | - testSplit('a', ['a'], 'a'); |
19 | | - testSplit('a/b', ['a', 'b'], 'a/b'); |
20 | | - testSplit('a/b/', ['a', 'b'], 'a/b'); |
21 | | - testSplit('a/b///', ['a', 'b'], 'a/b'); |
22 | | - testSplit('/', ['/'], '/'); |
23 | | - testSplit('///', ['/'], '/'); |
24 | | - testSplit('/a', ['/', 'a'], '/a'); |
25 | | - testSplit('///a', ['/', 'a'], '/a'); |
26 | | - testSplit('/a/b', ['/', 'a', 'b'], '/a/b'); |
27 | | - testSplit('a.js', ['a.js'], 'a.js'); |
28 | | - testSplit('/a.js/', ['/', 'a.js'], '/a.js'); |
29 | | - testSplit('/x/a.js/', ['/', 'x', 'a.js'], '/x/a.js'); |
30 | | - testSplit('///x/////a.js///', ['/', 'x', 'a.js'], '/x/a.js'); |
31 | | - testSplit( |
32 | | - '/project/My Module.module', |
33 | | - ['/', 'project', 'My Module.module'], |
34 | | - '/project/My Module.module' |
35 | | - ); |
36 | | - testSplit( |
37 | | - 'project/My Module.module/js', |
38 | | - ['project', 'My Module.module', 'js'], |
39 | | - 'project/My Module.module/js' |
40 | | - ); |
41 | | - testSplit( |
42 | | - 'project/My Module.module/js/main.js/', |
43 | | - ['project', 'My Module.module', 'js', 'main.js'], |
44 | | - 'project/My Module.module/js/main.js' |
45 | | - ); |
46 | | - testSplit( |
47 | | - 'project/My Module.module/js/../css/', |
48 | | - ['project', 'My Module.module', 'css'], |
49 | | - 'project/My Module.module/css' |
50 | | - ); |
51 | | - testSplit( |
52 | | - './project/My Module.module/js', |
53 | | - ['project', 'My Module.module', 'js'], |
54 | | - 'project/My Module.module/js' |
55 | | - ); |
56 | | - testSplit( |
57 | | - '../project/My Module.module/js', |
58 | | - ['..', 'project', 'My Module.module', 'js'], |
59 | | - '../project/My Module.module/js' |
60 | | - ); |
| 16 | +jest.mock('os', () => ({ |
| 17 | + homedir: jest.fn(), |
| 18 | +})); |
| 19 | + |
| 20 | +jest.mock('path', () => ({ |
| 21 | + ...jest.requireActual('path'), |
| 22 | + sep: '/', |
| 23 | + posix: { |
| 24 | + sep: '/', |
| 25 | + }, |
| 26 | + win32: { |
| 27 | + sep: '\\', |
| 28 | + }, |
| 29 | +})); |
| 30 | + |
| 31 | +describe('path utility functions', () => { |
| 32 | + describe('convertToUnixPath()', () => { |
| 33 | + test('converts Windows path to Unix path', () => { |
| 34 | + expect(convertToUnixPath('C:\\Users\\test\\file.txt')).toBe( |
| 35 | + '/Users/test/file.txt' |
| 36 | + ); |
| 37 | + }); |
| 38 | + |
| 39 | + test('normalizes Unix path', () => { |
| 40 | + expect(convertToUnixPath('/home//user/./file.txt')).toBe( |
| 41 | + '/home/user/file.txt' |
| 42 | + ); |
| 43 | + }); |
61 | 44 | }); |
| 45 | + |
| 46 | + describe('convertToLocalFileSystemPath()', () => { |
| 47 | + afterEach(() => { |
| 48 | + jest.resetModules(); |
| 49 | + }); |
| 50 | + |
| 51 | + test('converts to Unix path when on Unix-like system', async () => { |
| 52 | + jest.doMock('path', () => ({ ...jest.requireActual('path'), sep: '/' })); |
| 53 | + const { convertToLocalFileSystemPath } = await import('../path'); |
| 54 | + expect(convertToLocalFileSystemPath('/home/user/file.txt')).toBe( |
| 55 | + '/home/user/file.txt' |
| 56 | + ); |
| 57 | + }); |
| 58 | + |
| 59 | + test('converts to Windows path when on Windows system', async () => { |
| 60 | + jest.doMock('path', () => ({ ...jest.requireActual('path'), sep: '\\' })); |
| 61 | + const { convertToLocalFileSystemPath } = await import('../path'); |
| 62 | + expect(convertToLocalFileSystemPath('C:/Users/test/file.txt')).toBe( |
| 63 | + 'C:\\Users\\test\\file.txt' |
| 64 | + ); |
| 65 | + }); |
| 66 | + }); |
| 67 | + |
62 | 68 | describe('splitLocalPath()', () => { |
63 | | - function createTestSplit( |
64 | | - pathImplementation: PlatformPath |
65 | | - ): ( |
66 | | - filepath: string, |
67 | | - expectedParts: Array<string>, |
68 | | - joined: string |
69 | | - ) => void { |
70 | | - return ( |
71 | | - filepath: string, |
72 | | - expectedParts: Array<string>, |
73 | | - joined: string |
74 | | - ) => { |
75 | | - test(filepath, () => { |
76 | | - const parts = splitLocalPath(filepath, pathImplementation); |
77 | | - expect(parts).toEqual(expectedParts); |
78 | | - expect(pathImplementation.join(...parts)).toBe(joined); |
79 | | - }); |
80 | | - }; |
81 | | - } |
82 | | - |
83 | | - type TestCases = Array<[string, Array<string>, string]>; |
84 | | - |
85 | | - function getLocalFileSystemTestCases( |
86 | | - pathImplementation: PlatformPath |
87 | | - ): TestCases { |
88 | | - const { sep } = pathImplementation; |
89 | | - const isWin32 = sep === path.win32.sep; |
90 | | - const splitRoot = isWin32 ? 'C:' : '/'; |
91 | | - const pathRoot = isWin32 ? 'C:\\' : '/'; |
92 | | - return [ |
93 | | - [ |
94 | | - `${pathRoot}My Module.module`, |
95 | | - [splitRoot, 'My Module.module'], |
96 | | - `${pathRoot}My Module.module`, |
97 | | - ], |
98 | | - [ |
99 | | - `${pathRoot}My Module.module${sep}`, |
100 | | - [splitRoot, 'My Module.module'], |
101 | | - `${pathRoot}My Module.module`, |
102 | | - ], |
103 | | - [`My Module.module${sep}`, ['My Module.module'], 'My Module.module'], |
104 | | - [ |
105 | | - `${pathRoot}My Module.module${sep}js${sep}main.js`, |
106 | | - [splitRoot, 'My Module.module', 'js', 'main.js'], |
107 | | - `${pathRoot}My Module.module${sep}js${sep}main.js`, |
108 | | - ], |
109 | | - [ |
110 | | - `${pathRoot}My Module.module${sep}${sep}${sep}js${sep}main.js${sep}${sep}`, |
111 | | - [splitRoot, 'My Module.module', 'js', 'main.js'], |
112 | | - `${pathRoot}My Module.module${sep}js${sep}main.js`, |
113 | | - ], |
114 | | - [ |
115 | | - `${pathRoot}My Module.module${sep}js${sep}..${sep}css`, |
116 | | - [splitRoot, 'My Module.module', 'css'], |
117 | | - `${pathRoot}My Module.module${sep}css`, |
118 | | - ], |
119 | | - [ |
120 | | - `My Module.module${sep}js${sep}..${sep}css`, |
121 | | - ['My Module.module', 'css'], |
122 | | - `My Module.module${sep}css`, |
123 | | - ], |
124 | | - ]; |
125 | | - } |
126 | | - const platforms = ['posix', 'win32'] as const; |
127 | | - platforms.forEach(platform => { |
128 | | - describe(platform, () => { |
129 | | - const pathImplementation = path[platform]; |
130 | | - const testSplit = createTestSplit(pathImplementation); |
131 | | - const testCases = getLocalFileSystemTestCases(pathImplementation); |
132 | | - if (!(testCases && testCases.length)) { |
133 | | - throw new Error(`Missing ${platform} splitLocalPath() test cases`); |
134 | | - } |
135 | | - testCases.forEach(testCase => testSplit(...testCase)); |
136 | | - }); |
| 69 | + test('splits Unix path correctly', () => { |
| 70 | + expect( |
| 71 | + // eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 72 | + // @ts-ignore |
| 73 | + splitLocalPath('/home/user/file.txt', { |
| 74 | + sep: '/', |
| 75 | + normalize: (p: string) => p, |
| 76 | + }) |
| 77 | + ).toEqual(['/', 'home', 'user', 'file.txt']); |
| 78 | + }); |
| 79 | + |
| 80 | + test('splits Windows path correctly', () => { |
| 81 | + expect( |
| 82 | + // eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 83 | + // @ts-ignore |
| 84 | + splitLocalPath('C:\\Users\\test\\file.txt', { |
| 85 | + sep: '\\', |
| 86 | + normalize: (p: string) => p, |
| 87 | + }) |
| 88 | + ).toEqual(['C:', 'Users', 'test', 'file.txt']); |
| 89 | + }); |
| 90 | + |
| 91 | + test('handles empty path', () => { |
| 92 | + expect(splitLocalPath('')).toEqual([]); |
| 93 | + }); |
| 94 | + }); |
| 95 | + |
| 96 | + describe('splitHubSpotPath()', () => { |
| 97 | + test('splits HubSpot path correctly', () => { |
| 98 | + expect(splitHubSpotPath('/project/My Module.module/js/main.js')).toEqual([ |
| 99 | + '/', |
| 100 | + 'project', |
| 101 | + 'My Module.module', |
| 102 | + 'js', |
| 103 | + 'main.js', |
| 104 | + ]); |
| 105 | + }); |
| 106 | + |
| 107 | + test('handles root path', () => { |
| 108 | + expect(splitHubSpotPath('/')).toEqual(['/']); |
| 109 | + }); |
| 110 | + |
| 111 | + test('handles empty path', () => { |
| 112 | + expect(splitHubSpotPath('')).toEqual([]); |
| 113 | + }); |
| 114 | + }); |
| 115 | + |
| 116 | + describe('getCwd()', () => { |
| 117 | + const originalEnv = process.env; |
| 118 | + const originalCwd = process.cwd; |
| 119 | + |
| 120 | + beforeEach(() => { |
| 121 | + process.env = { ...originalEnv }; |
| 122 | + process.cwd = jest.fn().mockReturnValue('/mocked/cwd'); |
| 123 | + }); |
| 124 | + |
| 125 | + afterEach(() => { |
| 126 | + process.env = originalEnv; |
| 127 | + process.cwd = originalCwd; |
| 128 | + }); |
| 129 | + |
| 130 | + test('returns INIT_CWD if set', () => { |
| 131 | + process.env.INIT_CWD = '/custom/init/cwd'; |
| 132 | + expect(getCwd()).toBe('/custom/init/cwd'); |
| 133 | + }); |
| 134 | + |
| 135 | + test('returns process.cwd() if INIT_CWD not set', () => { |
| 136 | + delete process.env.INIT_CWD; |
| 137 | + expect(getCwd()).toBe('/mocked/cwd'); |
| 138 | + }); |
| 139 | + }); |
| 140 | + |
| 141 | + describe('getExt()', () => { |
| 142 | + test('returns lowercase extension without dot', () => { |
| 143 | + expect(getExt('file.TXT')).toBe('txt'); |
| 144 | + }); |
| 145 | + |
| 146 | + test('returns empty string for no extension', () => { |
| 147 | + expect(getExt('file')).toBe(''); |
| 148 | + }); |
| 149 | + |
| 150 | + test('handles non-string input', () => { |
| 151 | + // eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 152 | + // @ts-ignore |
| 153 | + expect(getExt(null as '')).toBe(''); |
| 154 | + }); |
| 155 | + }); |
| 156 | + |
| 157 | + describe('getAllowedExtensions()', () => { |
| 158 | + test('returns default allowed extensions', () => { |
| 159 | + const result = getAllowedExtensions(); |
| 160 | + expect(result).toBeInstanceOf(Set); |
| 161 | + expect(result).toEqual(new Set(ALLOWED_EXTENSIONS)); |
| 162 | + }); |
| 163 | + |
| 164 | + test('includes additional extensions', () => { |
| 165 | + const result = getAllowedExtensions(['custom']); |
| 166 | + expect(result.has('custom')).toBe(true); |
| 167 | + }); |
| 168 | + }); |
| 169 | + |
| 170 | + describe('isAllowedExtension()', () => { |
| 171 | + test('returns true for allowed extension', () => { |
| 172 | + expect(isAllowedExtension('file.txt')).toBe(true); |
| 173 | + }); |
| 174 | + |
| 175 | + test('returns false for disallowed extension', () => { |
| 176 | + expect(isAllowedExtension('file.exe')).toBe(false); |
| 177 | + }); |
| 178 | + |
| 179 | + test('allows custom extensions', () => { |
| 180 | + expect(isAllowedExtension('file.custom', ['custom'])).toBe(true); |
| 181 | + }); |
| 182 | + }); |
| 183 | + |
| 184 | + describe('sanitizeFileName()', () => { |
| 185 | + test('replaces invalid characters', () => { |
| 186 | + expect(sanitizeFileName('file:name?.txt')).toBe('file-name-.txt'); |
| 187 | + }); |
| 188 | + |
| 189 | + test('handles reserved names', () => { |
| 190 | + expect(sanitizeFileName('CON')).toBe('-CON'); |
| 191 | + }); |
| 192 | + |
| 193 | + test('removes trailing periods and spaces', () => { |
| 194 | + expect(sanitizeFileName('file.txt. ')).toBe('file.txt'); |
| 195 | + }); |
| 196 | + }); |
| 197 | + |
| 198 | + describe('isValidPath()', () => { |
| 199 | + test('returns true for valid path', () => { |
| 200 | + expect(isValidPath('/valid/path/file.txt')).toBe(true); |
| 201 | + }); |
| 202 | + |
| 203 | + test('returns false for path with invalid characters', () => { |
| 204 | + expect(isValidPath('/invalid/path/file?.txt')).toBe(false); |
| 205 | + }); |
| 206 | + |
| 207 | + test('returns false for reserved names', () => { |
| 208 | + expect(isValidPath('/some/path/CON')).toBe(false); |
| 209 | + }); |
| 210 | + }); |
| 211 | + |
| 212 | + describe('untildify()', () => { |
| 213 | + const originalHomedir = os.homedir; |
| 214 | + |
| 215 | + beforeEach(() => { |
| 216 | + (os.homedir as jest.Mock) = jest.fn().mockReturnValue('/home/user'); |
| 217 | + }); |
| 218 | + |
| 219 | + afterEach(() => { |
| 220 | + os.homedir = originalHomedir; |
| 221 | + }); |
| 222 | + |
| 223 | + test('replaces tilde with home directory', () => { |
| 224 | + expect(untildify('~/documents/file.txt')).toBe( |
| 225 | + '/home/user/documents/file.txt' |
| 226 | + ); |
| 227 | + }); |
| 228 | + |
| 229 | + test('does not modify paths without tilde', () => { |
| 230 | + expect(untildify('/absolute/path/file.txt')).toBe( |
| 231 | + '/absolute/path/file.txt' |
| 232 | + ); |
| 233 | + }); |
| 234 | + |
| 235 | + test('throws TypeError for non-string input', () => { |
| 236 | + // eslint-disable-next-line @typescript-eslint/ban-ts-comment |
| 237 | + // @ts-ignore |
| 238 | + expect(() => untildify(null as '')).toThrow(TypeError); |
137 | 239 | }); |
138 | 240 | }); |
139 | 241 | }); |
0 commit comments