File size: 7,349 Bytes
1dbc34b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { mkdirSafe, existsSafe } from '../src/fs-utils';

describe('fs-utils.ts', () => {
  let tempDir: string;

  beforeEach(async () => {
    // Create a temporary directory for testing
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-utils-test-'));
  });

  afterEach(async () => {
    // Clean up temporary directory
    try {
      await fs.rm(tempDir, { recursive: true, force: true });
    } catch (error) {
      // Ignore cleanup errors
    }
  });

  describe('mkdirSafe', () => {
    it('should create a new directory', async () => {
      const newDir = path.join(tempDir, 'new-directory');

      await mkdirSafe(newDir);

      const stats = await fs.stat(newDir);
      expect(stats.isDirectory()).toBe(true);
    });

    it('should create nested directories recursively', async () => {
      const nestedDir = path.join(tempDir, 'level1', 'level2', 'level3');

      await mkdirSafe(nestedDir);

      const stats = await fs.stat(nestedDir);
      expect(stats.isDirectory()).toBe(true);
    });

    it('should succeed when directory already exists', async () => {
      const existingDir = path.join(tempDir, 'existing');
      await fs.mkdir(existingDir);

      await expect(mkdirSafe(existingDir)).resolves.not.toThrow();
    });

    it('should succeed when path is a symlink to a directory', async () => {
      const targetDir = path.join(tempDir, 'target');
      const symlinkPath = path.join(tempDir, 'symlink');

      await fs.mkdir(targetDir);
      await fs.symlink(targetDir, symlinkPath, 'dir');

      await expect(mkdirSafe(symlinkPath)).resolves.not.toThrow();
    });

    it('should throw when path exists as a file', async () => {
      const filePath = path.join(tempDir, 'existing-file.txt');
      await fs.writeFile(filePath, 'content');

      await expect(mkdirSafe(filePath)).rejects.toThrow('Path exists and is not a directory');
    });

    it('should resolve relative paths', async () => {
      const originalCwd = process.cwd();
      try {
        process.chdir(tempDir);

        await mkdirSafe('relative-dir');

        const stats = await fs.stat(path.join(tempDir, 'relative-dir'));
        expect(stats.isDirectory()).toBe(true);
      } finally {
        process.chdir(originalCwd);
      }
    });

    it('should handle concurrent creation gracefully', async () => {
      const newDir = path.join(tempDir, 'concurrent');

      const promises = [mkdirSafe(newDir), mkdirSafe(newDir), mkdirSafe(newDir)];

      await expect(Promise.all(promises)).resolves.not.toThrow();

      const stats = await fs.stat(newDir);
      expect(stats.isDirectory()).toBe(true);
    });

    it('should handle paths with special characters', async () => {
      const specialDir = path.join(tempDir, 'dir with spaces & special-chars');

      await mkdirSafe(specialDir);

      const stats = await fs.stat(specialDir);
      expect(stats.isDirectory()).toBe(true);
    });
  });

  describe('existsSafe', () => {
    it('should return true for existing directory', async () => {
      const existingDir = path.join(tempDir, 'exists');
      await fs.mkdir(existingDir);

      const result = await existsSafe(existingDir);

      expect(result).toBe(true);
    });

    it('should return true for existing file', async () => {
      const filePath = path.join(tempDir, 'file.txt');
      await fs.writeFile(filePath, 'content');

      const result = await existsSafe(filePath);

      expect(result).toBe(true);
    });

    it('should return false for non-existent path', async () => {
      const nonExistent = path.join(tempDir, 'does-not-exist');

      const result = await existsSafe(nonExistent);

      expect(result).toBe(false);
    });

    it('should return true for symlink', async () => {
      const target = path.join(tempDir, 'target.txt');
      const symlink = path.join(tempDir, 'link.txt');

      await fs.writeFile(target, 'content');
      await fs.symlink(target, symlink);

      const result = await existsSafe(symlink);

      expect(result).toBe(true);
    });

    it('should return true for broken symlink', async () => {
      const symlink = path.join(tempDir, 'broken-link');

      // Create symlink to non-existent target
      await fs.symlink('/non/existent/path', symlink);

      const result = await existsSafe(symlink);

      // lstat succeeds on broken symlinks
      expect(result).toBe(true);
    });

    it('should handle relative paths', async () => {
      const originalCwd = process.cwd();
      try {
        process.chdir(tempDir);

        await fs.writeFile('test.txt', 'content');

        const result = await existsSafe('test.txt');

        expect(result).toBe(true);
      } finally {
        process.chdir(originalCwd);
      }
    });

    it('should handle paths with special characters', async () => {
      const specialFile = path.join(tempDir, 'file with spaces & chars.txt');
      await fs.writeFile(specialFile, 'content');

      const result = await existsSafe(specialFile);

      expect(result).toBe(true);
    });

    it('should return false for parent of non-existent nested path', async () => {
      const nonExistent = path.join(tempDir, 'does', 'not', 'exist');

      const result = await existsSafe(nonExistent);

      expect(result).toBe(false);
    });
  });

  describe('Error handling', () => {
    it('should handle permission errors in mkdirSafe', async () => {
      // Skip on Windows where permissions work differently
      if (process.platform === 'win32') {
        return;
      }

      const restrictedDir = path.join(tempDir, 'restricted');
      await fs.mkdir(restrictedDir);

      // Make directory read-only
      await fs.chmod(restrictedDir, 0o444);

      const newDir = path.join(restrictedDir, 'new');

      try {
        await expect(mkdirSafe(newDir)).rejects.toThrow();
      } finally {
        // Restore permissions for cleanup
        await fs.chmod(restrictedDir, 0o755);
      }
    });

    it('should propagate unexpected errors in existsSafe', async () => {
      const mockError = new Error('Unexpected error');
      (mockError as any).code = 'EACCES';

      const spy = vi.spyOn(fs, 'lstat').mockRejectedValueOnce(mockError);

      await expect(existsSafe('/some/path')).rejects.toThrow('Unexpected error');

      spy.mockRestore();
    });
  });

  describe('Integration scenarios', () => {
    it('should work together: check existence then create if missing', async () => {
      const dirPath = path.join(tempDir, 'check-then-create');

      const existsBefore = await existsSafe(dirPath);
      expect(existsBefore).toBe(false);

      await mkdirSafe(dirPath);

      const existsAfter = await existsSafe(dirPath);
      expect(existsAfter).toBe(true);
    });

    it('should handle nested directory creation with existence checks', async () => {
      const level1 = path.join(tempDir, 'level1');
      const level2 = path.join(level1, 'level2');
      const level3 = path.join(level2, 'level3');

      await mkdirSafe(level3);

      expect(await existsSafe(level1)).toBe(true);
      expect(await existsSafe(level2)).toBe(true);
      expect(await existsSafe(level3)).toBe(true);
    });
  });
});