Skip to content

Commit b65ab05

Browse files
authored
Merge pull request #622 from yamadashy/fix/is-not-a-dir
Improve user authentication flow
2 parents c13ee28 + 9494381 commit b65ab05

File tree

2 files changed

+154
-8
lines changed

2 files changed

+154
-8
lines changed

src/core/file/fileSearch.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Stats } from 'node:fs';
12
import fs from 'node:fs/promises';
23
import path from 'node:path';
34
import { globby } from 'globby';
@@ -92,7 +93,42 @@ export const normalizeGlobPattern = (pattern: string): string => {
9293

9394
// Get all file paths considering the config
9495
export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): Promise<FileSearchResult> => {
95-
// First check directory permissions
96+
// Check if the path exists and get its type
97+
let pathStats: Stats;
98+
try {
99+
pathStats = await fs.stat(rootDir);
100+
} catch (error) {
101+
if (error instanceof Error && 'code' in error) {
102+
const errorCode = (error as NodeJS.ErrnoException).code;
103+
if (errorCode === 'ENOENT') {
104+
throw new RepomixError(`Target path does not exist: ${rootDir}`);
105+
}
106+
if (errorCode === 'EPERM' || errorCode === 'EACCES') {
107+
throw new PermissionError(
108+
`Permission denied while accessing path. Please check folder access permissions for your terminal app. path: ${rootDir}`,
109+
rootDir,
110+
errorCode,
111+
);
112+
}
113+
// Handle other specific error codes with more context
114+
throw new RepomixError(`Failed to access path: ${rootDir}. Error code: ${errorCode}. ${error.message}`);
115+
}
116+
// Preserve original error stack trace for debugging
117+
const repomixError = new RepomixError(
118+
`Failed to access path: ${rootDir}. Reason: ${error instanceof Error ? error.message : JSON.stringify(error)}`,
119+
);
120+
repomixError.cause = error;
121+
throw repomixError;
122+
}
123+
124+
// Check if the path is a directory
125+
if (!pathStats.isDirectory()) {
126+
throw new RepomixError(
127+
`Target path is not a directory: ${rootDir}. Please specify a directory path, not a file path.`,
128+
);
129+
}
130+
131+
// Now check directory permissions
96132
const permissionCheck = await checkDirectoryPermissions(rootDir);
97133

98134
if (permissionCheck.details?.read !== true) {

tests/core/file/fileSearch.test.ts

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,41 @@ import {
1313
parseIgnoreContent,
1414
searchFiles,
1515
} from '../../../src/core/file/fileSearch.js';
16+
import { PermissionError } from '../../../src/core/file/permissionCheck.js';
17+
import { RepomixError } from '../../../src/shared/errorHandle.js';
1618
import { createMockConfig, isWindows } from '../../testing/testUtils.js';
1719

20+
import { checkDirectoryPermissions } from '../../../src/core/file/permissionCheck.js';
21+
1822
vi.mock('fs/promises');
1923
vi.mock('globby');
24+
vi.mock('../../../src/core/file/permissionCheck.js', () => ({
25+
checkDirectoryPermissions: vi.fn(),
26+
PermissionError: class extends Error {
27+
constructor(
28+
message: string,
29+
public readonly path: string,
30+
public readonly code?: string,
31+
) {
32+
super(message);
33+
this.name = 'PermissionError';
34+
}
35+
},
36+
}));
2037

2138
describe('fileSearch', () => {
2239
beforeEach(() => {
2340
vi.resetAllMocks();
41+
// Default mock for fs.stat to assume directory exists and is a directory
42+
vi.mocked(fs.stat).mockResolvedValue({
43+
isDirectory: () => true,
44+
isFile: () => false,
45+
} as Stats);
46+
// Default mock for checkDirectoryPermissions
47+
vi.mocked(checkDirectoryPermissions).mockResolvedValue({
48+
hasAllPermission: true,
49+
details: { read: true, write: true, execute: true },
50+
});
2451
});
2552

2653
describe('getIgnoreFilePaths', () => {
@@ -204,6 +231,15 @@ node_modules
204231
describe('filterFiles', () => {
205232
beforeEach(() => {
206233
vi.resetAllMocks();
234+
// Re-establish default mocks after reset
235+
vi.mocked(fs.stat).mockResolvedValue({
236+
isDirectory: () => true,
237+
isFile: () => false,
238+
} as Stats);
239+
vi.mocked(checkDirectoryPermissions).mockResolvedValue({
240+
hasAllPermission: true,
241+
details: { read: true, write: true, execute: true },
242+
});
207243
});
208244

209245
test('should call globby with correct parameters', async () => {
@@ -311,12 +347,24 @@ node_modules
311347
// Mock .git file content for worktree
312348
const gitWorktreeContent = 'gitdir: /path/to/main/repo/.git/worktrees/feature-branch';
313349

314-
// Mock fs.stat and fs.readFile for .git file
315-
vi.mocked(fs.stat).mockResolvedValue({
316-
isFile: () => true,
317-
} as Stats);
350+
// Mock fs.stat - first call for rootDir, subsequent calls for .git file
351+
vi.mocked(fs.stat)
352+
.mockResolvedValueOnce({
353+
isDirectory: () => true,
354+
isFile: () => false,
355+
} as Stats)
356+
.mockResolvedValue({
357+
isFile: () => true,
358+
isDirectory: () => false,
359+
} as Stats);
318360
vi.mocked(fs.readFile).mockResolvedValue(gitWorktreeContent);
319361

362+
// Override checkDirectoryPermissions mock for this test
363+
vi.mocked(checkDirectoryPermissions).mockResolvedValue({
364+
hasAllPermission: true,
365+
details: { read: true, write: true, execute: true },
366+
});
367+
320368
// Mock globby to return some test files
321369
vi.mocked(globby).mockResolvedValue(['file1.js', 'file2.js']);
322370

@@ -345,9 +393,21 @@ node_modules
345393

346394
test('should handle regular git repository correctly', async () => {
347395
// Mock .git as a directory
348-
vi.mocked(fs.stat).mockResolvedValue({
349-
isFile: () => false,
350-
} as Stats);
396+
vi.mocked(fs.stat)
397+
.mockResolvedValueOnce({
398+
isDirectory: () => true,
399+
isFile: () => false,
400+
} as Stats)
401+
.mockResolvedValue({
402+
isFile: () => false,
403+
isDirectory: () => true,
404+
} as Stats);
405+
406+
// Override checkDirectoryPermissions mock for this test
407+
vi.mocked(checkDirectoryPermissions).mockResolvedValue({
408+
hasAllPermission: true,
409+
details: { read: true, write: true, execute: true },
410+
});
351411

352412
// Mock globby to return some test files
353413
vi.mocked(globby).mockResolvedValue(['file1.js', 'file2.js']);
@@ -453,4 +513,54 @@ node_modules
453513
expect(normalizeGlobPattern('**/folder/**/*')).toBe('**/folder/**/*');
454514
});
455515
});
516+
517+
describe('searchFiles path validation', () => {
518+
test('should throw error when target path does not exist', async () => {
519+
const error = new Error('ENOENT') as Error & { code: string };
520+
error.code = 'ENOENT';
521+
vi.mocked(fs.stat).mockRejectedValue(error);
522+
523+
const mockConfig = createMockConfig();
524+
525+
await expect(searchFiles('/nonexistent/path', mockConfig)).rejects.toThrow(RepomixError);
526+
await expect(searchFiles('/nonexistent/path', mockConfig)).rejects.toThrow(
527+
'Target path does not exist: /nonexistent/path',
528+
);
529+
});
530+
531+
test('should throw PermissionError when access is denied', async () => {
532+
const error = new Error('EPERM') as Error & { code: string };
533+
error.code = 'EPERM';
534+
vi.mocked(fs.stat).mockRejectedValue(error);
535+
536+
const mockConfig = createMockConfig();
537+
538+
await expect(searchFiles('/forbidden/path', mockConfig)).rejects.toThrow(PermissionError);
539+
});
540+
541+
test('should throw error when target path is a file, not a directory', async () => {
542+
vi.mocked(fs.stat).mockResolvedValue({
543+
isDirectory: () => false,
544+
isFile: () => true,
545+
} as Stats);
546+
547+
const mockConfig = createMockConfig();
548+
549+
await expect(searchFiles('/path/to/file.txt', mockConfig)).rejects.toThrow(RepomixError);
550+
await expect(searchFiles('/path/to/file.txt', mockConfig)).rejects.toThrow(
551+
'Target path is not a directory: /path/to/file.txt. Please specify a directory path, not a file path.',
552+
);
553+
});
554+
555+
test('should succeed when target path is a valid directory', async () => {
556+
vi.mocked(globby).mockResolvedValue(['test.js']);
557+
558+
const mockConfig = createMockConfig();
559+
560+
const result = await searchFiles('/valid/directory', mockConfig);
561+
562+
expect(result.filePaths).toEqual(['test.js']);
563+
expect(result.emptyDirPaths).toEqual([]);
564+
});
565+
});
456566
});

0 commit comments

Comments
 (0)