Skip to content

Commit 82f4519

Browse files
committed
feat: add tests for chmod-items handler and mock filesystem interactions
1 parent 82a4c50 commit 82f4519

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { vi, describe, it, expect, beforeEach } from 'vitest';
2+
3+
// 設置模擬模塊
4+
vi.mock('node:fs', () => ({
5+
promises: {
6+
chmod: vi.fn().mockName('fs.chmod')
7+
}
8+
}));
9+
10+
vi.mock('../../src/utils/path-utils', () => ({
11+
resolvePath: vi.fn().mockImplementation((path) =>
12+
`/project-root/${path}`
13+
).mockName('pathUtils.resolvePath'),
14+
PROJECT_ROOT: '/project-root'
15+
}));
16+
17+
describe('chmod-items handler', () => {
18+
let handler: any;
19+
let fsMock: any;
20+
let pathUtilsMock: any;
21+
22+
beforeEach(async () => {
23+
// 動態導入模擬模塊
24+
fsMock = (await import('node:fs')).promises;
25+
pathUtilsMock = await import('../../src/utils/path-utils');
26+
27+
// 重置模擬
28+
vi.resetAllMocks();
29+
30+
// 設置默認模擬實現
31+
pathUtilsMock.resolvePath.mockImplementation((path: string) =>
32+
`/project-root/${path}`
33+
);
34+
fsMock.chmod.mockResolvedValue(undefined);
35+
36+
// 動態導入處理程序
37+
const { chmodItemsToolDefinition } = await import('../../src/handlers/chmod-items');
38+
handler = chmodItemsToolDefinition.handler;
39+
});
40+
41+
it('should change permissions for valid paths', async () => {
42+
const result = await handler({
43+
paths: ['file1.txt', 'dir/file2.txt'],
44+
mode: '755'
45+
});
46+
47+
expect(fsMock.chmod).toHaveBeenCalledTimes(2);
48+
expect(JSON.parse(result.content[0].text)).toEqual([
49+
{ path: 'file1.txt', mode: '755', success: true },
50+
{ path: 'dir/file2.txt', mode: '755', success: true }
51+
]);
52+
});
53+
54+
it('should handle multiple operations with mixed results', async () => {
55+
fsMock.chmod
56+
.mockResolvedValueOnce(undefined)
57+
.mockRejectedValueOnce({ code: 'EPERM' });
58+
59+
const result = await handler({
60+
paths: ['file1.txt', 'file2.txt'],
61+
mode: '755'
62+
});
63+
64+
const output = JSON.parse(result.content[0].text);
65+
expect(output[0].success).toBe(true);
66+
expect(output[1].success).toBe(false);
67+
});
68+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { formatStats, FormattedStats } from '../../src/utils/stats-utils';
3+
4+
function makeMockStats(partial: Partial<Record<keyof FormattedStats, any>> = {}): any {
5+
// Provide default values and allow overrides
6+
return {
7+
isFile: () => partial.isFile ?? true,
8+
isDirectory: () => partial.isDirectory ?? false,
9+
isSymbolicLink: () => partial.isSymbolicLink ?? false,
10+
size: partial.size ?? 1234,
11+
atime: partial.atime ?? new Date('2024-01-01T01:02:03.000Z'),
12+
mtime: partial.mtime ?? new Date('2024-01-02T01:02:03.000Z'),
13+
ctime: partial.ctime ?? new Date('2024-01-03T01:02:03.000Z'),
14+
birthtime: partial.birthtime ?? new Date('2024-01-04T01:02:03.000Z'),
15+
mode: partial.mode ?? 0o755,
16+
uid: partial.uid ?? 1000,
17+
gid: partial.gid ?? 1000,
18+
};
19+
}
20+
21+
describe('formatStats', () => {
22+
it('formats a regular file', () => {
23+
const stats = makeMockStats({ isFile: true, isDirectory: false, isSymbolicLink: false, mode: 0o644 });
24+
const result = formatStats('foo\\bar.txt', '/abs/foo/bar.txt', stats as any);
25+
expect(result).toEqual({
26+
path: 'foo/bar.txt',
27+
isFile: true,
28+
isDirectory: false,
29+
isSymbolicLink: false,
30+
size: 1234,
31+
atime: '2024-01-01T01:02:03.000Z',
32+
mtime: '2024-01-02T01:02:03.000Z',
33+
ctime: '2024-01-03T01:02:03.000Z',
34+
birthtime: '2024-01-04T01:02:03.000Z',
35+
mode: '644',
36+
uid: 1000,
37+
gid: 1000,
38+
});
39+
});
40+
41+
it('formats a directory', () => {
42+
const stats = makeMockStats({ isFile: false, isDirectory: true, isSymbolicLink: false, mode: 0o755 });
43+
const result = formatStats('dir\\', '/abs/dir', stats as any);
44+
expect(result.isDirectory).toBe(true);
45+
expect(result.isFile).toBe(false);
46+
expect(result.mode).toBe('755');
47+
});
48+
49+
it('formats a symbolic link', () => {
50+
const stats = makeMockStats({ isFile: false, isDirectory: false, isSymbolicLink: true, mode: 0o777 });
51+
const result = formatStats('link', '/abs/link', stats as any);
52+
expect(result.isSymbolicLink).toBe(true);
53+
expect(result.mode).toBe('777');
54+
});
55+
56+
it('pads mode with leading zeros', () => {
57+
const stats = makeMockStats({ mode: 0o7 });
58+
const result = formatStats('file', '/abs/file', stats as any);
59+
expect(result.mode).toBe('007');
60+
});
61+
62+
it('converts all date fields to ISO string', () => {
63+
const stats = makeMockStats({
64+
atime: new Date('2020-01-01T00:00:00.000Z'),
65+
mtime: new Date('2020-01-02T00:00:00.000Z'),
66+
ctime: new Date('2020-01-03T00:00:00.000Z'),
67+
birthtime: new Date('2020-01-04T00:00:00.000Z'),
68+
});
69+
const result = formatStats('file', '/abs/file', stats as any);
70+
expect(result.atime).toBe('2020-01-01T00:00:00.000Z');
71+
expect(result.mtime).toBe('2020-01-02T00:00:00.000Z');
72+
expect(result.ctime).toBe('2020-01-03T00:00:00.000Z');
73+
expect(result.birthtime).toBe('2020-01-04T00:00:00.000Z');
74+
});
75+
76+
it('replaces backslashes in path with forward slashes', () => {
77+
const stats = makeMockStats();
78+
const result = formatStats('foo\\bar\\baz.txt', '/abs/foo/bar/baz.txt', stats as any);
79+
expect(result.path).toBe('foo/bar/baz.txt');
80+
});
81+
});

0 commit comments

Comments
 (0)