fileditor-mcp 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,290 +1,290 @@
1
- import { SetWorkspaceHandler } from '../src/handlers/setWorkspace.js';
2
- import { FileUtils } from '../src/utils/fileUtils.js';
3
- import { describe, it, beforeEach, after } from 'node:test';
4
- import assert from 'node:assert';
5
- import { tmpdir } from 'os';
6
- import { join } from 'path';
7
- import { promises as fs } from 'fs';
8
-
9
- /**
10
- * Test suite for SetWorkspaceHandler.
11
- * Tests the workspace setting functionality.
12
- */
13
- describe('SetWorkspaceHandler', () => {
14
-
15
- beforeEach(() => {
16
- // Reset workspace state
17
- FileUtils.setWorkspaceRoot(process.cwd());
18
- });
19
-
20
- describe('handle method', () => {
21
- it('should successfully set the workspace root directory', async () => {
22
- const testPath = process.cwd(); // Use the current directory, ensure it exists
23
- const args = { path: testPath };
24
-
25
- const result = await SetWorkspaceHandler.handle(args);
26
-
27
- assert.strictEqual(result.content[0].type, 'text');
28
- assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
29
- assert.ok(result.content[0].text.includes(testPath));
30
- assert.strictEqual(FileUtils.getWorkspaceRoot(), testPath);
31
- }); it('should handle Windows path format', async () => {
32
- if (process.platform !== 'win32') {
33
- // Skip this test on non-Windows platforms
34
- console.log('Skipping Windows path format test: not on a Windows platform');
35
- return;
36
- }
37
- const testPath = 'C:\\'; // Use the root directory, usually exists on Windows
38
- const args = { path: testPath };
39
-
40
- try {
41
- const result = await SetWorkspaceHandler.handle(args);
42
-
43
- assert.strictEqual(result.content[0].type, 'text');
44
- assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
45
- } catch (error) {
46
- // If C:\ does not exist (very rare), it should throw a directory not found error
47
- assert.ok(error.message.includes('Workspace directory does not exist'));
48
- }
49
- }); it('should handle relative paths', async () => {
50
- const testPath = '.'; // Use the current directory
51
- const args = { path: testPath };
52
-
53
- const result = await SetWorkspaceHandler.handle(args);
54
-
55
- assert.strictEqual(result.content[0].type, 'text');
56
- assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
57
- // Relative paths are resolved to absolute paths
58
- assert.ok(FileUtils.getWorkspaceRoot().includes(process.cwd()));
59
- }); it('should throw an error when the directory does not exist', async () => {
60
- const testPath = './nonexistent/directory';
61
- const args = { path: testPath };
62
-
63
- try {
64
- await SetWorkspaceHandler.handle(args);
65
- assert.fail('Should have thrown an error');
66
- } catch (error) {
67
- assert.ok(error.message.includes('Workspace directory does not exist'));
68
- }
69
- }); it('should throw an error when the path contains a non-existent directory with special characters', async () => {
70
- const testPath = '/path/with spaces/and-symbols_123';
71
- const args = { path: testPath };
72
-
73
- try {
74
- await SetWorkspaceHandler.handle(args);
75
- assert.fail('Should have thrown an error');
76
- } catch (error) {
77
- assert.ok(error.message.includes('Workspace directory does not exist'));
78
- }
79
- });
80
-
81
- it('should throw an error when the path parameter is missing', async () => {
82
- const args = {};
83
-
84
- try {
85
- await SetWorkspaceHandler.handle(args);
86
- assert.fail('Should have thrown an error');
87
- } catch (error) {
88
- assert.ok(error.message.includes('Failed to set workspace'));
89
- }
90
- });
91
-
92
- });
93
-
94
- describe('Edge cases', () => {
95
-
96
- it('should handle null path', async () => {
97
- const args = { path: null };
98
-
99
- try {
100
- await SetWorkspaceHandler.handle(args);
101
- // If no error is thrown, check the result
102
- assert.strictEqual(FileUtils.getWorkspaceRoot(), null);
103
- } catch (error) {
104
- // If an error is thrown, ensure it is a reasonable error message
105
- assert.ok(error.message.includes('Failed to set workspace'));
106
- }
107
- });
108
-
109
- it('should handle undefined path', async () => {
110
- const args = { path: undefined };
111
-
112
- try {
113
- await SetWorkspaceHandler.handle(args);
114
- assert.fail('Should have thrown an error');
115
- } catch (error) {
116
- assert.ok(error.message.includes('Failed to set workspace'));
117
- }
118
- });
119
-
120
- });
121
-
122
- describe('Path security tests', () => {
123
- let testWorkspace;
124
-
125
- beforeEach(async () => {
126
- testWorkspace = join(tmpdir(), `mcp-security-test-${Date.now()}`);
127
- await fs.mkdir(testWorkspace, { recursive: true });
128
-
129
- // Create test directory structure
130
- await fs.mkdir(join(testWorkspace, 'safe'), { recursive: true });
131
- await fs.mkdir(join(testWorkspace, 'safe', 'nested'), { recursive: true });
132
- await fs.writeFile(join(testWorkspace, 'safe', 'test.txt'), 'safe content');
133
- await fs.writeFile(join(testWorkspace, 'safe', 'nested', 'deep.txt'), 'deep content');
134
-
135
- // Create a file outside the workspace
136
- await fs.writeFile(join(tmpdir(), 'outside.txt'), 'outside content');
137
-
138
- FileUtils.setWorkspaceRoot(testWorkspace);
139
- });
140
-
141
- after(async () => {
142
- try {
143
- await fs.rm(testWorkspace, { recursive: true, force: true });
144
- await fs.rm(join(tmpdir(), 'outside.txt'), { force: true });
145
- } catch (error) {
146
- // Ignore cleanup errors
147
- }
148
- });
149
-
150
- it('should succeed for normal path access', async () => {
151
- // Test relative path
152
- const safePath1 = FileUtils.getSecurePath('safe/test.txt');
153
- assert.ok(safePath1.includes('test.txt'));
154
-
155
- // Test nested path
156
- const safePath2 = FileUtils.getSecurePath('safe/nested/deep.txt');
157
- assert.ok(safePath2.includes('deep.txt'));
158
-
159
- // Test current directory
160
- const safePath3 = FileUtils.getSecurePath('.');
161
- assert.strictEqual(safePath3, testWorkspace);
162
- });
163
-
164
- it('should prevent path traversal attacks', async () => {
165
- const traversalAttempts = [
166
- '../outside.txt', // Basic traversal
167
- '../../outside.txt', // Deep traversal
168
- 'safe/../../../outside.txt', // Mixed traversal
169
- 'safe/../../outside.txt', // Nested traversal
170
- '/etc/passwd', // Absolute path attack (Unix)
171
- ];
172
- if (process.platform === 'win32') {
173
- traversalAttempts.push(
174
- 'C:\\Windows\\System32\\config\\SAM', // Absolute path attack (Windows)
175
- '..\\..\\outside.txt', // Windows-style traversal
176
- 'safe\\..\\..\\outside.txt', // Windows mixed traversal
177
- );
178
- }
179
- for (const maliciousPath of traversalAttempts) {
180
- await assert.rejects(
181
- async () => FileUtils.getSecurePath(maliciousPath),
182
- (error) => {
183
- return error.message.includes('Access denied') ||
184
- error.message.includes('outside the workspace');
185
- },
186
- `Path traversal attack should be blocked: ${maliciousPath}`
187
- );
188
- }
189
- });
190
-
191
- it('should reject dangerous characters', async () => {
192
- const dangerousInputs = [
193
- 'test\0file.txt', // Null byte
194
- 'test\x00file.txt', // Hex null byte
195
- '', // Empty string
196
- null, // Null value
197
- undefined, // Undefined value
198
- 123, // Number type
199
- ];
200
-
201
- for (const dangerousInput of dangerousInputs) {
202
- await assert.rejects(
203
- async () => FileUtils.getSecurePath(dangerousInput),
204
- (error) => {
205
- return error.message.includes('Invalid file path') ||
206
- error.message.includes('null bytes');
207
- },
208
- `Dangerous input should be rejected: ${dangerousInput}`
209
- );
210
- }
211
- });
212
-
213
- // it('should prevent symlink traversal', async () => {
214
- // // Create a symlink pointing outside the workspace
215
- // const symlinkPath = join(testWorkspace, 'malicious_link');
216
- // const outsidePath = join(tmpdir(), 'outside.txt');
217
- //
218
- // try {
219
- // await fs.symlink(outsidePath, symlinkPath);
220
- //
221
- // // Attempt to access the file outside the workspace via the symlink
222
- // await assert.rejects(
223
- // async () => FileUtils.getSecurePath('malicious_link'),
224
- // (error) => error.message.includes('Access denied') ||
225
- // error.message.includes('outside the workspace'),
226
- // 'Symlink traversal attack should be blocked'
227
- // );
228
- // } catch (error) {
229
- // // Some systems may not support creating symlinks, skip this test
230
- // if (error.code === 'EPERM' || error.code === 'ENOENT') {
231
- // console.log('Skipping symlink test: system does not support or permission denied');
232
- // return;
233
- // }
234
- // throw error;
235
- // }
236
- // });
237
-
238
- it('should work correctly for workspace validation', async () => {
239
- // Test invalid workspace settings
240
- await assert.rejects(
241
- async () => FileUtils.setWorkspaceRoot(''),
242
- (error) => error.message.includes('Invalid workspace root'),
243
- 'Empty workspace path should be rejected'
244
- );
245
-
246
- await assert.rejects(
247
- async () => FileUtils.setWorkspaceRoot(null),
248
- (error) => error.message.includes('Invalid workspace root'),
249
- 'Null workspace path should be rejected'
250
- );
251
-
252
- await assert.rejects(
253
- async () => FileUtils.setWorkspaceRoot('/nonexistent/directory'),
254
- (error) => error.message.includes('does not exist'),
255
- 'Non-existent directory should be rejected'
256
- );
257
-
258
- // Test file as workspace
259
- const tempFile = join(tmpdir(), `test-file-${Date.now()}.txt`);
260
- await fs.writeFile(tempFile, 'test');
261
-
262
- try {
263
- await assert.rejects(
264
- async () => FileUtils.setWorkspaceRoot(tempFile),
265
- (error) => error.message.includes('not a directory'),
266
- 'File path as workspace should be rejected'
267
- );
268
- } finally {
269
- await fs.rm(tempFile, { force: true });
270
- }
271
- });
272
-
273
- it('should deny access when workspace is not set', async () => {
274
- // Temporarily clear workspace setting
275
- const originalWorkspace = FileUtils.WORKSPACE_ROOT;
276
- FileUtils.WORKSPACE_ROOT = null;
277
-
278
- try {
279
- await assert.rejects(
280
- async () => FileUtils.getSecurePath('test.txt'),
281
- (error) => error.message.includes('Workspace not set'),
282
- 'Access should be denied when workspace is not set'
283
- );
284
- } finally {
285
- // Restore workspace setting
286
- FileUtils.WORKSPACE_ROOT = originalWorkspace;
287
- }
288
- });
289
- });
290
- });
1
+ import { SetWorkspaceHandler } from '../src/handlers/setWorkspace.js';
2
+ import { FileUtils } from '../src/utils/fileUtils.js';
3
+ import { describe, it, beforeEach, after } from 'node:test';
4
+ import assert from 'node:assert';
5
+ import { tmpdir } from 'os';
6
+ import { join } from 'path';
7
+ import { promises as fs } from 'fs';
8
+
9
+ /**
10
+ * Test suite for SetWorkspaceHandler.
11
+ * Tests the workspace setting functionality.
12
+ */
13
+ describe('SetWorkspaceHandler', () => {
14
+
15
+ beforeEach(() => {
16
+ // Reset workspace state
17
+ FileUtils.setWorkspaceRoot(process.cwd());
18
+ });
19
+
20
+ describe('handle method', () => {
21
+ it('should successfully set the workspace root directory', async () => {
22
+ const testPath = process.cwd(); // Use the current directory, ensure it exists
23
+ const args = { path: testPath };
24
+
25
+ const result = await SetWorkspaceHandler.handle(args);
26
+
27
+ assert.strictEqual(result.content[0].type, 'text');
28
+ assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
29
+ assert.ok(result.content[0].text.includes(testPath));
30
+ assert.strictEqual(FileUtils.getWorkspaceRoot(), testPath);
31
+ }); it('should handle Windows path format', async () => {
32
+ if (process.platform !== 'win32') {
33
+ // Skip this test on non-Windows platforms
34
+ console.log('Skipping Windows path format test: not on a Windows platform');
35
+ return;
36
+ }
37
+ const testPath = 'C:\\'; // Use the root directory, usually exists on Windows
38
+ const args = { path: testPath };
39
+
40
+ try {
41
+ const result = await SetWorkspaceHandler.handle(args);
42
+
43
+ assert.strictEqual(result.content[0].type, 'text');
44
+ assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
45
+ } catch (error) {
46
+ // If C:\ does not exist (very rare), it should throw a directory not found error
47
+ assert.ok(error.message.includes('Workspace directory does not exist'));
48
+ }
49
+ }); it('should handle relative paths', async () => {
50
+ const testPath = '.'; // Use the current directory
51
+ const args = { path: testPath };
52
+
53
+ const result = await SetWorkspaceHandler.handle(args);
54
+
55
+ assert.strictEqual(result.content[0].type, 'text');
56
+ assert.ok(result.content[0].text.includes('Successfully set workspace root to:'));
57
+ // Relative paths are resolved to absolute paths
58
+ assert.ok(FileUtils.getWorkspaceRoot().includes(process.cwd()));
59
+ }); it('should throw an error when the directory does not exist', async () => {
60
+ const testPath = './nonexistent/directory';
61
+ const args = { path: testPath };
62
+
63
+ try {
64
+ await SetWorkspaceHandler.handle(args);
65
+ assert.fail('Should have thrown an error');
66
+ } catch (error) {
67
+ assert.ok(error.message.includes('Workspace directory does not exist'));
68
+ }
69
+ }); it('should throw an error when the path contains a non-existent directory with special characters', async () => {
70
+ const testPath = '/path/with spaces/and-symbols_123';
71
+ const args = { path: testPath };
72
+
73
+ try {
74
+ await SetWorkspaceHandler.handle(args);
75
+ assert.fail('Should have thrown an error');
76
+ } catch (error) {
77
+ assert.ok(error.message.includes('Workspace directory does not exist'));
78
+ }
79
+ });
80
+
81
+ it('should throw an error when the path parameter is missing', async () => {
82
+ const args = {};
83
+
84
+ try {
85
+ await SetWorkspaceHandler.handle(args);
86
+ assert.fail('Should have thrown an error');
87
+ } catch (error) {
88
+ assert.ok(error.message.includes('Failed to set workspace'));
89
+ }
90
+ });
91
+
92
+ });
93
+
94
+ describe('Edge cases', () => {
95
+
96
+ it('should handle null path', async () => {
97
+ const args = { path: null };
98
+
99
+ try {
100
+ await SetWorkspaceHandler.handle(args);
101
+ // If no error is thrown, check the result
102
+ assert.strictEqual(FileUtils.getWorkspaceRoot(), null);
103
+ } catch (error) {
104
+ // If an error is thrown, ensure it is a reasonable error message
105
+ assert.ok(error.message.includes('Failed to set workspace'));
106
+ }
107
+ });
108
+
109
+ it('should handle undefined path', async () => {
110
+ const args = { path: undefined };
111
+
112
+ try {
113
+ await SetWorkspaceHandler.handle(args);
114
+ assert.fail('Should have thrown an error');
115
+ } catch (error) {
116
+ assert.ok(error.message.includes('Failed to set workspace'));
117
+ }
118
+ });
119
+
120
+ });
121
+
122
+ describe('Path security tests', () => {
123
+ let testWorkspace;
124
+
125
+ beforeEach(async () => {
126
+ testWorkspace = join(tmpdir(), `mcp-security-test-${Date.now()}`);
127
+ await fs.mkdir(testWorkspace, { recursive: true });
128
+
129
+ // Create test directory structure
130
+ await fs.mkdir(join(testWorkspace, 'safe'), { recursive: true });
131
+ await fs.mkdir(join(testWorkspace, 'safe', 'nested'), { recursive: true });
132
+ await fs.writeFile(join(testWorkspace, 'safe', 'test.txt'), 'safe content');
133
+ await fs.writeFile(join(testWorkspace, 'safe', 'nested', 'deep.txt'), 'deep content');
134
+
135
+ // Create a file outside the workspace
136
+ await fs.writeFile(join(tmpdir(), 'outside.txt'), 'outside content');
137
+
138
+ FileUtils.setWorkspaceRoot(testWorkspace);
139
+ });
140
+
141
+ after(async () => {
142
+ try {
143
+ await fs.rm(testWorkspace, { recursive: true, force: true });
144
+ await fs.rm(join(tmpdir(), 'outside.txt'), { force: true });
145
+ } catch (error) {
146
+ // Ignore cleanup errors
147
+ }
148
+ });
149
+
150
+ it('should succeed for normal path access', async () => {
151
+ // Test relative path
152
+ const safePath1 = FileUtils.getSecurePath('safe/test.txt');
153
+ assert.ok(safePath1.includes('test.txt'));
154
+
155
+ // Test nested path
156
+ const safePath2 = FileUtils.getSecurePath('safe/nested/deep.txt');
157
+ assert.ok(safePath2.includes('deep.txt'));
158
+
159
+ // Test current directory
160
+ const safePath3 = FileUtils.getSecurePath('.');
161
+ assert.strictEqual(safePath3, testWorkspace);
162
+ });
163
+
164
+ it('should prevent path traversal attacks', async () => {
165
+ const traversalAttempts = [
166
+ '../outside.txt', // Basic traversal
167
+ '../../outside.txt', // Deep traversal
168
+ 'safe/../../../outside.txt', // Mixed traversal
169
+ 'safe/../../outside.txt', // Nested traversal
170
+ '/etc/passwd', // Absolute path attack (Unix)
171
+ ];
172
+ if (process.platform === 'win32') {
173
+ traversalAttempts.push(
174
+ 'C:\\Windows\\System32\\config\\SAM', // Absolute path attack (Windows)
175
+ '..\\..\\outside.txt', // Windows-style traversal
176
+ 'safe\\..\\..\\outside.txt', // Windows mixed traversal
177
+ );
178
+ }
179
+ for (const maliciousPath of traversalAttempts) {
180
+ await assert.rejects(
181
+ async () => FileUtils.getSecurePath(maliciousPath),
182
+ (error) => {
183
+ return error.message.includes('Access denied') ||
184
+ error.message.includes('outside the workspace');
185
+ },
186
+ `Path traversal attack should be blocked: ${maliciousPath}`
187
+ );
188
+ }
189
+ });
190
+
191
+ it('should reject dangerous characters', async () => {
192
+ const dangerousInputs = [
193
+ 'test\0file.txt', // Null byte
194
+ 'test\x00file.txt', // Hex null byte
195
+ '', // Empty string
196
+ null, // Null value
197
+ undefined, // Undefined value
198
+ 123, // Number type
199
+ ];
200
+
201
+ for (const dangerousInput of dangerousInputs) {
202
+ await assert.rejects(
203
+ async () => FileUtils.getSecurePath(dangerousInput),
204
+ (error) => {
205
+ return error.message.includes('Invalid file path') ||
206
+ error.message.includes('null bytes');
207
+ },
208
+ `Dangerous input should be rejected: ${dangerousInput}`
209
+ );
210
+ }
211
+ });
212
+
213
+ // it('should prevent symlink traversal', async () => {
214
+ // // Create a symlink pointing outside the workspace
215
+ // const symlinkPath = join(testWorkspace, 'malicious_link');
216
+ // const outsidePath = join(tmpdir(), 'outside.txt');
217
+ //
218
+ // try {
219
+ // await fs.symlink(outsidePath, symlinkPath);
220
+ //
221
+ // // Attempt to access the file outside the workspace via the symlink
222
+ // await assert.rejects(
223
+ // async () => FileUtils.getSecurePath('malicious_link'),
224
+ // (error) => error.message.includes('Access denied') ||
225
+ // error.message.includes('outside the workspace'),
226
+ // 'Symlink traversal attack should be blocked'
227
+ // );
228
+ // } catch (error) {
229
+ // // Some systems may not support creating symlinks, skip this test
230
+ // if (error.code === 'EPERM' || error.code === 'ENOENT') {
231
+ // console.log('Skipping symlink test: system does not support or permission denied');
232
+ // return;
233
+ // }
234
+ // throw error;
235
+ // }
236
+ // });
237
+
238
+ it('should work correctly for workspace validation', async () => {
239
+ // Test invalid workspace settings
240
+ await assert.rejects(
241
+ async () => FileUtils.setWorkspaceRoot(''),
242
+ (error) => error.message.includes('Invalid workspace root'),
243
+ 'Empty workspace path should be rejected'
244
+ );
245
+
246
+ await assert.rejects(
247
+ async () => FileUtils.setWorkspaceRoot(null),
248
+ (error) => error.message.includes('Invalid workspace root'),
249
+ 'Null workspace path should be rejected'
250
+ );
251
+
252
+ await assert.rejects(
253
+ async () => FileUtils.setWorkspaceRoot('/nonexistent/directory'),
254
+ (error) => error.message.includes('does not exist'),
255
+ 'Non-existent directory should be rejected'
256
+ );
257
+
258
+ // Test file as workspace
259
+ const tempFile = join(tmpdir(), `test-file-${Date.now()}.txt`);
260
+ await fs.writeFile(tempFile, 'test');
261
+
262
+ try {
263
+ await assert.rejects(
264
+ async () => FileUtils.setWorkspaceRoot(tempFile),
265
+ (error) => error.message.includes('not a directory'),
266
+ 'File path as workspace should be rejected'
267
+ );
268
+ } finally {
269
+ await fs.rm(tempFile, { force: true });
270
+ }
271
+ });
272
+
273
+ it('should deny access when workspace is not set', async () => {
274
+ // Temporarily clear workspace setting
275
+ const originalWorkspace = FileUtils.WORKSPACE_ROOT;
276
+ FileUtils.WORKSPACE_ROOT = null;
277
+
278
+ try {
279
+ await assert.rejects(
280
+ async () => FileUtils.getSecurePath('test.txt'),
281
+ (error) => error.message.includes('Workspace not set'),
282
+ 'Access should be denied when workspace is not set'
283
+ );
284
+ } finally {
285
+ // Restore workspace setting
286
+ FileUtils.WORKSPACE_ROOT = originalWorkspace;
287
+ }
288
+ });
289
+ });
290
+ });