| 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 () => { |
| |
| tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'fs-utils-test-')); |
| }); |
|
|
| afterEach(async () => { |
| |
| try { |
| await fs.rm(tempDir, { recursive: true, force: true }); |
| } catch (error) { |
| |
| } |
| }); |
|
|
| 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'); |
|
|
| |
| await fs.symlink('/non/existent/path', symlink); |
|
|
| const result = await existsSafe(symlink); |
|
|
| |
| 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 () => { |
| |
| if (process.platform === 'win32') { |
| return; |
| } |
|
|
| const restrictedDir = path.join(tempDir, 'restricted'); |
| await fs.mkdir(restrictedDir); |
|
|
| |
| await fs.chmod(restrictedDir, 0o444); |
|
|
| const newDir = path.join(restrictedDir, 'new'); |
|
|
| try { |
| await expect(mkdirSafe(newDir)).rejects.toThrow(); |
| } finally { |
| |
| 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); |
| }); |
| }); |
| }); |
|
|