File size: 5,786 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 | import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdirSafe, existsSafe } from '@automaker/utils';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
describe('fs-utils.ts', () => {
let testDir: string;
beforeEach(async () => {
// Create a temporary test directory
testDir = path.join(os.tmpdir(), `fs-utils-test-${Date.now()}`);
await fs.mkdir(testDir, { recursive: true });
});
afterEach(async () => {
// Clean up test directory
try {
await fs.rm(testDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
describe('mkdirSafe', () => {
it('should create a new directory', async () => {
const newDir = path.join(testDir, 'new-directory');
await mkdirSafe(newDir);
const stats = await fs.stat(newDir);
expect(stats.isDirectory()).toBe(true);
});
it('should succeed if directory already exists', async () => {
const existingDir = path.join(testDir, 'existing');
await fs.mkdir(existingDir);
// Should not throw
await expect(mkdirSafe(existingDir)).resolves.toBeUndefined();
});
it('should create nested directories', async () => {
const nestedDir = path.join(testDir, 'a', 'b', 'c');
await mkdirSafe(nestedDir);
const stats = await fs.stat(nestedDir);
expect(stats.isDirectory()).toBe(true);
});
it('should throw if path exists as a file', async () => {
const filePath = path.join(testDir, 'file.txt');
await fs.writeFile(filePath, 'content');
await expect(mkdirSafe(filePath)).rejects.toThrow('Path exists and is not a directory');
});
it('should succeed if path is a symlink to a directory', async () => {
const realDir = path.join(testDir, 'real-dir');
const symlinkPath = path.join(testDir, 'link-to-dir');
await fs.mkdir(realDir);
await fs.symlink(realDir, symlinkPath);
// Should not throw
await expect(mkdirSafe(symlinkPath)).resolves.toBeUndefined();
});
it('should handle ELOOP error gracefully when checking path', async () => {
// Mock lstat to throw ELOOP error
const originalLstat = fs.lstat;
const mkdirSafePath = path.join(testDir, 'eloop-path');
vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ELOOP' });
// Should not throw, should return gracefully
await expect(mkdirSafe(mkdirSafePath)).resolves.toBeUndefined();
vi.restoreAllMocks();
});
it('should handle EEXIST error gracefully when creating directory', async () => {
const newDir = path.join(testDir, 'race-condition-dir');
// Mock lstat to return ENOENT (path doesn't exist)
// Then mock mkdir to throw EEXIST (race condition)
vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' });
vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'EEXIST' });
// Should not throw, should return gracefully
await expect(mkdirSafe(newDir)).resolves.toBeUndefined();
vi.restoreAllMocks();
});
it('should handle ELOOP error gracefully when creating directory', async () => {
const newDir = path.join(testDir, 'eloop-create-dir');
// Mock lstat to return ENOENT (path doesn't exist)
// Then mock mkdir to throw ELOOP
vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ENOENT' });
vi.spyOn(fs, 'mkdir').mockRejectedValueOnce({ code: 'ELOOP' });
// Should not throw, should return gracefully
await expect(mkdirSafe(newDir)).resolves.toBeUndefined();
vi.restoreAllMocks();
});
});
describe('existsSafe', () => {
it('should return true for existing file', async () => {
const filePath = path.join(testDir, 'test-file.txt');
await fs.writeFile(filePath, 'content');
const exists = await existsSafe(filePath);
expect(exists).toBe(true);
});
it('should return true for existing directory', async () => {
const dirPath = path.join(testDir, 'test-dir');
await fs.mkdir(dirPath);
const exists = await existsSafe(dirPath);
expect(exists).toBe(true);
});
it('should return false for non-existent path', async () => {
const nonExistent = path.join(testDir, 'does-not-exist');
const exists = await existsSafe(nonExistent);
expect(exists).toBe(false);
});
it('should return true for symlink', async () => {
const realFile = path.join(testDir, 'real-file.txt');
const symlinkPath = path.join(testDir, 'link-to-file');
await fs.writeFile(realFile, 'content');
await fs.symlink(realFile, symlinkPath);
const exists = await existsSafe(symlinkPath);
expect(exists).toBe(true);
});
it("should return true for broken symlink (symlink exists even if target doesn't)", async () => {
const symlinkPath = path.join(testDir, 'broken-link');
const nonExistent = path.join(testDir, 'non-existent-target');
await fs.symlink(nonExistent, symlinkPath);
const exists = await existsSafe(symlinkPath);
expect(exists).toBe(true);
});
it('should return true for ELOOP error (symlink loop)', async () => {
// Mock lstat to throw ELOOP error
vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'ELOOP' });
const exists = await existsSafe('/some/path/with/loop');
expect(exists).toBe(true);
vi.restoreAllMocks();
});
it('should throw for other errors', async () => {
// Mock lstat to throw a non-ENOENT, non-ELOOP error
vi.spyOn(fs, 'lstat').mockRejectedValueOnce({ code: 'EACCES' });
await expect(existsSafe('/some/path')).rejects.toMatchObject({ code: 'EACCES' });
vi.restoreAllMocks();
});
});
});
|