result = new HashMap<>();
28 |
29 | try {
30 | Path filePath = Paths.get(path);
31 |
32 | // Create parent directories if they don't exist
33 | Path parent = filePath.getParent();
34 | if (parent != null && !Files.exists(parent)) {
35 | Files.createDirectories(parent);
36 | result.put("createdDirectories", parent.toString());
37 | }
38 |
39 | // Write the content to the file
40 | boolean fileExisted = Files.exists(filePath);
41 | Files.writeString(filePath, content, StandardCharsets.UTF_8,
42 | StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
43 |
44 | result.put("path", path);
45 | result.put("bytesWritten", content.getBytes(StandardCharsets.UTF_8).length);
46 | result.put("action", fileExisted ? "overwritten" : "created");
47 |
48 | return successMessage(result);
49 |
50 | } catch (IOException e) {
51 | return errorMessage("Failed to write file: " + e.getMessage());
52 | } catch (Exception e) {
53 | return errorMessage("Failed to serialize error result");
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/src/main/resources/META-INF/native-image/native-image.properties:
--------------------------------------------------------------------------------
1 | Args = \
2 | --no-fallback \
3 | --enable-url-protocols=https \
4 | --enable-http \
5 | --enable-https \
6 | --report-unsupported-elements-at-runtime \
7 | --initialize-at-build-time=org.slf4j,ch.qos.logback \
8 | -H:+ReportExceptionStackTraces
9 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/native-image/reflect-config.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "com.devoxx.mcp.filesystem.McpServerApplication",
4 | "allDeclaredConstructors": true,
5 | "allPublicConstructors": true,
6 | "allDeclaredMethods": true,
7 | "allPublicMethods": true
8 | },
9 | {
10 | "name": "com.devoxx.mcp.filesystem.tools.EditFileService",
11 | "allDeclaredConstructors": true,
12 | "allPublicConstructors": true,
13 | "allDeclaredMethods": true,
14 | "allPublicMethods": true
15 | },
16 | {
17 | "name": "com.devoxx.mcp.filesystem.tools.WriteFileService",
18 | "allDeclaredConstructors": true,
19 | "allPublicConstructors": true,
20 | "allDeclaredMethods": true,
21 | "allPublicMethods": true
22 | },
23 | {
24 | "name": "com.devoxx.mcp.filesystem.tools.ReadFileService",
25 | "allDeclaredConstructors": true,
26 | "allPublicConstructors": true,
27 | "allDeclaredMethods": true,
28 | "allPublicMethods": true
29 | },
30 | {
31 | "name": "com.devoxx.mcp.filesystem.tools.FetchWebpageService",
32 | "allDeclaredConstructors": true,
33 | "allPublicConstructors": true,
34 | "allDeclaredMethods": true,
35 | "allPublicMethods": true
36 | },
37 | {
38 | "name": "com.devoxx.mcp.filesystem.tools.SearchFilesService",
39 | "allDeclaredConstructors": true,
40 | "allPublicConstructors": true,
41 | "allDeclaredMethods": true,
42 | "allPublicMethods": true
43 | },
44 | {
45 | "name": "com.devoxx.mcp.filesystem.tools.ListDirectoryService",
46 | "allDeclaredConstructors": true,
47 | "allPublicConstructors": true,
48 | "allDeclaredMethods": true,
49 | "allPublicMethods": true
50 | }
51 | ]
--------------------------------------------------------------------------------
/src/main/resources/META-INF/native-image/resource-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "resources": {
3 | "includes": [
4 | {"pattern": "\\QMETA-INF/services/.*\\E"},
5 | {"pattern": "\\Qapplication.properties\\E"},
6 | {"pattern": "\\Qmcp-servers-config.json\\E"}
7 | ]
8 | }
9 | }
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | # spring.main.web-application-type=none
2 |
3 | # NOTE: You must disable the banner and the console logging
4 | # to allow the STDIO transport to work !!!
5 | spring.main.banner-mode=off
6 | logging.pattern.console=
7 |
8 | # spring.ai.mcp.server.stdio=true
9 |
10 | spring.ai.mcp.server.name=filesystem-server
11 | spring.ai.mcp.server.version=0.0.1
12 |
13 | logging.file.name=./target/filesystem-server.log
14 |
15 |
--------------------------------------------------------------------------------
/src/main/resources/mcp-servers-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "devoxx-filesystem": {
4 | "command": "java",
5 | "args": [
6 | "-Dspring.ai.mcp.server.stdio=true",
7 | "-Dspring.main.web-application-type=none",
8 | "-Dlogging.pattern.console=",
9 | "-jar",
10 | "/Users/stephan/IdeaProjects/JavaFileSystemMCP/target/target/devoxx-filesystem-0.0.1-SNAPSHOT.jar"
11 | ],
12 | "env": {}
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/ClientSse.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 - 2024 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.devoxx.mcp.filesystem;
17 |
18 | import io.modelcontextprotocol.client.McpClient;
19 | import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
20 | import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
21 |
22 | /**
23 | * @author Christian Tzolov
24 | */
25 | public class ClientSse {
26 |
27 | public static void main(String[] args) {
28 | var transport = new HttpClientSseClientTransport("http://localhost:8080");
29 |
30 | var client = McpClient.sync(transport).build();
31 |
32 | client.initialize();
33 |
34 | // List and demonstrate tools
35 | ListToolsResult toolsList = client.listTools();
36 | System.out.println("Available Tools = " + toolsList);
37 |
38 | client.closeGracefully();
39 |
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/ClientStdio.java:
--------------------------------------------------------------------------------
1 | package com.devoxx.mcp.filesystem;
2 |
3 | import java.util.Map;
4 |
5 | import io.modelcontextprotocol.client.McpClient;
6 | import io.modelcontextprotocol.client.transport.ServerParameters;
7 | import io.modelcontextprotocol.client.transport.StdioClientTransport;
8 | import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
9 | import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
10 | import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
11 |
12 | /**
13 | * With stdio transport, the MCP server is automatically started by the client.
14 | * But you
15 | * have to build the server jar first:
16 | *
17 | *
18 | * ./mvnw clean install -DskipTests
19 | *
20 | */
21 | public class ClientStdio {
22 |
23 | public static void main(String[] args) throws InterruptedException {
24 |
25 | var stdioParams = ServerParameters.builder("java")
26 | .args("-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none",
27 | "-Dlogging.pattern.console=", "-jar",
28 | "/Users/christiantzolov/Dev/projects/demo/MCPJavaFileSystem/target/devoxx-filesystem-0.0.1-SNAPSHOT.jar")
29 | .build();
30 |
31 | var transport = new StdioClientTransport(stdioParams);
32 | var client = McpClient.sync(transport).build();
33 |
34 | client.initialize();
35 |
36 | // List and demonstrate tools
37 | ListToolsResult toolsList = client.listTools();
38 | System.out.println("Available Tools = " + toolsList);
39 |
40 | client.closeGracefully();
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/tools/BashServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.devoxx.mcp.filesystem.tools;
2 |
3 | import org.junit.jupiter.api.BeforeEach;
4 | import org.junit.jupiter.api.Test;
5 | import org.junit.jupiter.api.io.TempDir;
6 |
7 | import java.io.File;
8 | import java.io.IOException;
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 |
12 | import static org.junit.jupiter.api.Assertions.*;
13 |
14 | class BashServiceTest {
15 |
16 | private BashService bashService;
17 |
18 | @TempDir
19 | Path tempDir;
20 |
21 | @BeforeEach
22 | void setUp() {
23 | bashService = new BashService();
24 | }
25 |
26 | @Test
27 | void shouldExecuteSimpleCommand() {
28 | // When
29 | String result = bashService.executeBash("echo 'Hello World'", null, null);
30 |
31 | // Then
32 | assertTrue(result.contains("Hello World"));
33 | assertTrue(result.contains("\"exitCode\":0"));
34 | assertTrue(result.contains("\"success\":true"));
35 | }
36 |
37 | @Test
38 | void shouldHandleWorkingDirectory() throws IOException {
39 | // Given
40 | File testFile = new File(tempDir.toFile(), "test.txt");
41 | Files.writeString(testFile.toPath(), "test content");
42 |
43 | // When
44 | String result = bashService.executeBash("cat test.txt", tempDir.toString(), null);
45 |
46 | // Then
47 | assertTrue(result.contains("test content"));
48 | assertTrue(result.contains("\"exitCode\":0"));
49 | }
50 |
51 | @Test
52 | void shouldHandleCommandTimeout() {
53 | // When
54 | String result = bashService.executeBash("sleep 3", null, 1);
55 |
56 | // Then
57 | assertTrue(result.contains("timed out"));
58 | assertTrue(result.contains("\"success\":false"));
59 | }
60 |
61 | @Test
62 | void shouldHandleCommandFailure() {
63 | // When
64 | String result = bashService.executeBash("ls /nonexistent_directory", null, null);
65 |
66 | // Then
67 | assertTrue(result.contains("\"exitCode\":"));
68 | assertFalse(result.contains("\"exitCode\":0"));
69 | }
70 |
71 | @Test
72 | void shouldHandleEmptyCommand() {
73 | // When
74 | String result = bashService.executeBash("", null, null);
75 |
76 | // Then
77 | assertTrue(result.contains("\"success\":false"));
78 | assertTrue(result.contains("Command cannot be empty"));
79 | }
80 |
81 | @Test
82 | void shouldCaptureCommandOutput() {
83 | // When
84 | String result = bashService.executeBash("echo 'Line 1' && echo 'Line 2'", null, null);
85 |
86 | // Then
87 | assertTrue(result.contains("Line 1"));
88 | assertTrue(result.contains("Line 2"));
89 | assertTrue(result.contains("\"exitCode\":0"));
90 | }
91 | }
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/tools/CreateDirectoryServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.devoxx.mcp.filesystem.tools;
2 |
3 | import com.fasterxml.jackson.databind.JsonNode;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.io.TempDir;
8 |
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 |
12 | import static org.junit.jupiter.api.Assertions.*;
13 |
14 | class CreateDirectoryServiceTest {
15 |
16 | private CreateDirectoryService createDirectoryService;
17 | private ObjectMapper objectMapper;
18 |
19 | @TempDir
20 | Path tempDir;
21 |
22 | @BeforeEach
23 | void setUp() {
24 | createDirectoryService = new CreateDirectoryService();
25 | objectMapper = new ObjectMapper();
26 | }
27 |
28 | @Test
29 | void shouldSuccessfullyCreateSingleDirectory() throws Exception {
30 | // Given
31 | Path testDir = tempDir.resolve("test-directory");
32 | String testDirPath = testDir.toString();
33 | String[] directories = new String[] { testDirPath };
34 |
35 | // When
36 | String result = createDirectoryService.createDirectory(directories);
37 | JsonNode jsonResult = objectMapper.readTree(result);
38 |
39 | // Then
40 | assertTrue(jsonResult.get("success").asBoolean());
41 | assertTrue(Files.exists(testDir));
42 | assertTrue(Files.isDirectory(testDir));
43 | }
44 |
45 | @Test
46 | void shouldSuccessfullyCreateMultipleDirectories() throws Exception {
47 | // Given
48 | Path testDir1 = tempDir.resolve("test-dir-1");
49 | Path testDir2 = tempDir.resolve("test-dir-2");
50 | Path testDir3 = tempDir.resolve("test-dir-3");
51 |
52 | String[] directories = new String[] {
53 | testDir1.toString(),
54 | testDir2.toString(),
55 | testDir3.toString()
56 | };
57 |
58 | // When
59 | String result = createDirectoryService.createDirectory(directories);
60 | JsonNode jsonResult = objectMapper.readTree(result);
61 |
62 | // Then
63 | assertTrue(jsonResult.get("success").asBoolean());
64 | assertTrue(Files.exists(testDir1));
65 | assertTrue(Files.exists(testDir2));
66 | assertTrue(Files.exists(testDir3));
67 | assertTrue(Files.isDirectory(testDir1));
68 | assertTrue(Files.isDirectory(testDir2));
69 | assertTrue(Files.isDirectory(testDir3));
70 | }
71 |
72 | @Test
73 | void shouldCreateNestedDirectoryStructure() throws Exception {
74 | // Given
75 | Path nestedDir = tempDir.resolve("parent/child/grandchild");
76 | String[] directories = new String[] { nestedDir.toString() };
77 |
78 | // When
79 | String result = createDirectoryService.createDirectory(directories);
80 | JsonNode jsonResult = objectMapper.readTree(result);
81 |
82 | // Then
83 | assertTrue(jsonResult.get("success").asBoolean());
84 | assertTrue(Files.exists(nestedDir));
85 | assertTrue(Files.isDirectory(nestedDir));
86 | assertTrue(Files.exists(nestedDir.getParent()));
87 | assertTrue(Files.exists(nestedDir.getParent().getParent()));
88 | }
89 |
90 | @Test
91 | void shouldSucceedWhenDirectoryAlreadyExists() throws Exception {
92 | // Given
93 | Path existingDir = tempDir.resolve("existing-directory");
94 | Files.createDirectory(existingDir);
95 | String[] directories = new String[] { existingDir.toString() };
96 |
97 | // When
98 | String result = createDirectoryService.createDirectory(directories);
99 | JsonNode jsonResult = objectMapper.readTree(result);
100 |
101 | // Then
102 | assertTrue(jsonResult.get("success").asBoolean());
103 | assertTrue(Files.exists(existingDir));
104 | assertTrue(Files.isDirectory(existingDir));
105 | }
106 |
107 | @Test
108 | void shouldHandleEmptyDirectoryList() throws Exception {
109 | // Given
110 | String[] directories = new String[0];
111 |
112 | // When
113 | String result = createDirectoryService.createDirectory(directories);
114 | JsonNode jsonResult = objectMapper.readTree(result);
115 |
116 | // Then
117 | assertTrue(jsonResult.get("success").asBoolean());
118 | }
119 |
120 | @Test
121 | void shouldHandleSpecialCharactersInDirectoryName() throws Exception {
122 | // Given
123 | Path specialNameDir = tempDir.resolve("special_char-dir-ñäöü");
124 | String[] directories = new String[] { specialNameDir.toString() };
125 |
126 | // When
127 | String result = createDirectoryService.createDirectory(directories);
128 | JsonNode jsonResult = objectMapper.readTree(result);
129 |
130 | // Then
131 | assertTrue(jsonResult.get("success").asBoolean());
132 | assertTrue(Files.exists(specialNameDir));
133 | assertTrue(Files.isDirectory(specialNameDir));
134 | }
135 |
136 | @Test
137 | void shouldHandlePathsWithSpaces() throws Exception {
138 | // Given
139 | Path dirWithSpaces = tempDir.resolve("dir with spaces in name");
140 | String[] directories = new String[] { dirWithSpaces.toString() };
141 |
142 | // When
143 | String result = createDirectoryService.createDirectory(directories);
144 | JsonNode jsonResult = objectMapper.readTree(result);
145 |
146 | // Then
147 | assertTrue(jsonResult.get("success").asBoolean());
148 | assertTrue(Files.exists(dirWithSpaces));
149 | assertTrue(Files.isDirectory(dirWithSpaces));
150 | }
151 | }
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/tools/EditFileServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.devoxx.mcp.filesystem.tools;
2 |
3 | import com.fasterxml.jackson.databind.JsonNode;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.Nested;
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.io.TempDir;
10 |
11 | import java.nio.file.Files;
12 | import java.nio.file.Path;
13 |
14 | import static org.junit.jupiter.api.Assertions.*;
15 |
16 | class EditFileServiceTest {
17 |
18 | private EditFileService editFileService;
19 | private ObjectMapper objectMapper;
20 |
21 | @TempDir
22 | Path tempDir;
23 |
24 | @BeforeEach
25 | void setUp() {
26 | editFileService = new EditFileService();
27 | objectMapper = new ObjectMapper();
28 | }
29 |
30 | @Nested
31 | @DisplayName("Basic Functionality Tests")
32 | class BasicFunctionalityTests {
33 | @Test
34 | void shouldSuccessfullyApplySingleEdit() throws Exception {
35 | // Given
36 | String initialContent = "Line 1\nLine 2\nLine 3\nLine 4";
37 | Path testFile = tempDir.resolve("test-edit.txt");
38 | Files.writeString(testFile, initialContent);
39 |
40 | String edits = "[{\"oldText\":\"Line 2\",\"newText\":\"Modified Line 2\"}]";
41 |
42 | // When
43 | String result = editFileService.editFile(testFile.toString(), edits, false);
44 | JsonNode jsonResult = objectMapper.readTree(result);
45 |
46 | // Then
47 | assertTrue(jsonResult.get("success").asBoolean());
48 | assertEquals(1, jsonResult.get("editsApplied").asInt());
49 | assertFalse(jsonResult.get("dryRun").asBoolean());
50 |
51 | // Verify file was actually modified
52 | String expectedContent = "Line 1\nModified Line 2\nLine 3\nLine 4";
53 | assertEquals(expectedContent, Files.readString(testFile));
54 | }
55 |
56 | @Test
57 | void shouldSuccessfullyApplyMultipleEdits() throws Exception {
58 | // Given
59 | String initialContent = "Line 1\nLine 2\nLine 3\nLine 4";
60 | Path testFile = tempDir.resolve("test-multiple-edits.txt");
61 | Files.writeString(testFile, initialContent);
62 |
63 | String edits = "[" +
64 | "{\"oldText\":\"Line 1\",\"newText\":\"Modified Line 1\"}," +
65 | "{\"oldText\":\"Line 3\",\"newText\":\"Modified Line 3\"}" +
66 | "]";
67 |
68 | // When
69 | String result = editFileService.editFile(testFile.toString(), edits, false);
70 | JsonNode jsonResult = objectMapper.readTree(result);
71 |
72 | // Then
73 | assertTrue(jsonResult.get("success").asBoolean());
74 | assertEquals(2, jsonResult.get("editsApplied").asInt());
75 |
76 | // Verify file was actually modified with both edits
77 | String expectedContent = "Modified Line 1\nLine 2\nModified Line 3\nLine 4";
78 | assertEquals(expectedContent, Files.readString(testFile));
79 | }
80 |
81 | @Test
82 | void shouldHandleDryRunMode() throws Exception {
83 | // Given
84 | String initialContent = "Line 1\nLine 2\nLine 3";
85 | Path testFile = tempDir.resolve("test-dry-run.txt");
86 | Files.writeString(testFile, initialContent);
87 |
88 | String edits = "[{\"oldText\":\"Line 2\",\"newText\":\"Should Not Change\"}]";
89 |
90 | // When
91 | String result = editFileService.editFile(testFile.toString(), edits, true);
92 | JsonNode jsonResult = objectMapper.readTree(result);
93 |
94 | // Then
95 | assertTrue(jsonResult.get("success").asBoolean());
96 | assertTrue(jsonResult.get("dryRun").asBoolean());
97 | assertTrue(jsonResult.has("diff"));
98 |
99 | // Verify file was NOT modified since this was a dry run
100 | assertEquals(initialContent, Files.readString(testFile));
101 | }
102 |
103 | @Test
104 | void shouldHandleNonExistentFile() throws Exception {
105 | // Given
106 | Path nonExistentFile = tempDir.resolve("non-existent.txt");
107 | String edits = "[{\"oldText\":\"Any Text\",\"newText\":\"New Text\"}]";
108 |
109 | // When
110 | String result = editFileService.editFile(nonExistentFile.toString(), edits, false);
111 | JsonNode jsonResult = objectMapper.readTree(result);
112 |
113 | // Then
114 | assertFalse(jsonResult.get("success").asBoolean());
115 | assertTrue(jsonResult.get("error").asText().contains("File does not exist"));
116 | }
117 |
118 | @Test
119 | void shouldHandleNonMatchingText() throws Exception {
120 | // Given
121 | String initialContent = "Line 1\nLine 2\nLine 3";
122 | Path testFile = tempDir.resolve("non-matching.txt");
123 | Files.writeString(testFile, initialContent);
124 |
125 | String edits = "[{\"oldText\":\"This text does not exist\",\"newText\":\"Replacement\"}]";
126 |
127 | // When
128 | String result = editFileService.editFile(testFile.toString(), edits, false);
129 | JsonNode jsonResult = objectMapper.readTree(result);
130 |
131 | // Then
132 | assertFalse(jsonResult.get("success").asBoolean());
133 | assertTrue(jsonResult.get("error").asText().contains("Could not find text to replace"));
134 |
135 | // Verify file was not modified
136 | assertEquals(initialContent, Files.readString(testFile));
137 | }
138 |
139 | @Test
140 | void shouldHandleMultilineTextReplacement() throws Exception {
141 | // Given
142 | String initialContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
143 | Path testFile = tempDir.resolve("multiline.txt");
144 | Files.writeString(testFile, initialContent);
145 |
146 | String edits = "[{\"oldText\":\"Line 2\\nLine 3\\nLine 4\",\"newText\":\"Replaced multiple lines\"}]";
147 |
148 | // When
149 | String result = editFileService.editFile(testFile.toString(), edits, false);
150 | JsonNode jsonResult = objectMapper.readTree(result);
151 |
152 | // Then
153 | assertTrue(jsonResult.get("success").asBoolean());
154 |
155 | // Verify multiline replacement worked
156 | String expectedContent = "Line 1\nReplaced multiple lines\nLine 5";
157 | assertEquals(expectedContent, Files.readString(testFile));
158 | }
159 | }
160 |
161 | @Nested
162 | @DisplayName("Input Format Tests")
163 | class InputFormatTests {
164 |
165 | @Test
166 | void shouldHandleSingleJsonObjectFormat() throws Exception {
167 | // Given
168 | String initialContent = "Test single object format";
169 | Path testFile = tempDir.resolve("single-object.txt");
170 | Files.writeString(testFile, initialContent);
171 |
172 | // Single JSON object format
173 | String edits = "{\"oldText\":\"single object\",\"newText\":\"JSON object\"}";
174 |
175 | // When
176 | String result = editFileService.editFile(testFile.toString(), edits, false);
177 | JsonNode jsonResult = objectMapper.readTree(result);
178 |
179 | // Then
180 | assertTrue(jsonResult.get("success").asBoolean());
181 |
182 | // Verify replacement worked
183 | String expectedContent = "Test JSON object format";
184 | assertEquals(expectedContent, Files.readString(testFile));
185 | }
186 |
187 | @Test
188 | void shouldHandleJsonArrayFormat() throws Exception {
189 | // Given
190 | String initialContent = "First text and second text";
191 | Path testFile = tempDir.resolve("json-array.txt");
192 | Files.writeString(testFile, initialContent);
193 |
194 | // JSON array format
195 | String edits = "[{\"oldText\":\"First\",\"newText\":\"1st\"},{\"oldText\":\"second\",\"newText\":\"2nd\"}]";
196 |
197 | // When
198 | String result = editFileService.editFile(testFile.toString(), edits, false);
199 | JsonNode jsonResult = objectMapper.readTree(result);
200 |
201 | // Then
202 | assertTrue(jsonResult.get("success").asBoolean());
203 | assertEquals(2, jsonResult.get("editsApplied").asInt());
204 |
205 | // Verify both replacements worked
206 | String expectedContent = "1st text and 2nd text";
207 | assertEquals(expectedContent, Files.readString(testFile));
208 | }
209 |
210 | @Test
211 | void shouldHandleSimpleTextFormat() throws Exception {
212 | // Given
213 | String initialContent = "Simple format test";
214 | Path testFile = tempDir.resolve("simple-format.txt");
215 | Files.writeString(testFile, initialContent);
216 |
217 | // Simple text format with ---- separator
218 | String edits = "Simple format----Better format";
219 |
220 | // When
221 | String result = editFileService.editFile(testFile.toString(), edits, false);
222 | JsonNode jsonResult = objectMapper.readTree(result);
223 |
224 | // Then
225 | assertTrue(jsonResult.get("success").asBoolean());
226 |
227 | // Verify replacement worked
228 | String expectedContent = "Better format test";
229 | assertEquals(expectedContent, Files.readString(testFile));
230 | }
231 |
232 | // TODO
233 | void shouldHandleMalformedEditsJson() throws Exception {
234 | // Given
235 | String initialContent = "Test content";
236 | Path testFile = tempDir.resolve("malformed-json.txt");
237 | Files.writeString(testFile, initialContent);
238 |
239 | String edits = "This is not valid JSON or Simple format";
240 |
241 | // When
242 | String result = editFileService.editFile(testFile.toString(), edits, false);
243 | JsonNode jsonResult = objectMapper.readTree(result);
244 |
245 | // Then
246 | assertFalse(jsonResult.get("success").asBoolean());
247 | assertEquals("This is not valid JSON or Simple format", jsonResult.get("debug_received_edits").asText());
248 |
249 | // Verify file was not modified
250 | assertEquals(initialContent, Files.readString(testFile));
251 | }
252 |
253 | @Test
254 | void shouldHandleIncompleteEditsObject() throws Exception {
255 | // Given
256 | String initialContent = "Test content";
257 | Path testFile = tempDir.resolve("incomplete-json.txt");
258 | Files.writeString(testFile, initialContent);
259 |
260 | // Missing newText field
261 | String edits = "[{\"oldText\":\"Test content\"}]";
262 |
263 | // When
264 | String result = editFileService.editFile(testFile.toString(), edits, false);
265 | JsonNode jsonResult = objectMapper.readTree(result);
266 |
267 | // Then
268 | assertFalse(jsonResult.get("success").asBoolean());
269 |
270 | // Verify file was not modified
271 | assertEquals(initialContent, Files.readString(testFile));
272 | }
273 | }
274 |
275 | @Nested
276 | @DisplayName("Line Ending and Whitespace Tests")
277 | class LineEndingTests {
278 |
279 | @Test
280 | void shouldHandleDifferentLineEndings() throws Exception {
281 | // Given
282 | String initialContent = "Windows\r\nLine\rEndings\nMixed";
283 | Path testFile = tempDir.resolve("line-endings.txt");
284 | Files.writeString(testFile, initialContent);
285 |
286 | // Test different line endings in the edit
287 | String edits = "{\"oldText\":\"Windows\\r\\nLine\\rEndings\",\"newText\":\"Normalized Line Endings\"}";
288 |
289 | // When
290 | String result = editFileService.editFile(testFile.toString(), edits, false);
291 | JsonNode jsonResult = objectMapper.readTree(result);
292 |
293 | // Then
294 | assertTrue(jsonResult.get("success").asBoolean());
295 |
296 | // Verify replacement worked despite different line endings
297 | String expectedContent = "Normalized Line Endings\nMixed";
298 | assertEquals(expectedContent, Files.readString(testFile));
299 | }
300 |
301 | @Test
302 | void shouldHandleLeadingAndTrailingWhitespace() throws Exception {
303 | // Given
304 | String initialContent = "Text with spaces and tabs\t\t";
305 | Path testFile = tempDir.resolve("whitespace.txt");
306 | Files.writeString(testFile, initialContent);
307 |
308 | String edits = "{\"oldText\":\"spaces and tabs\",\"newText\":\"minimal whitespace\"}";
309 |
310 | // When
311 | String result = editFileService.editFile(testFile.toString(), edits, false);
312 | JsonNode jsonResult = objectMapper.readTree(result);
313 |
314 | // Then
315 | assertTrue(jsonResult.get("success").asBoolean());
316 |
317 | // Verify replacement preserved whitespace correctly
318 | String expectedContent = "Text with minimal whitespace\t\t";
319 | assertEquals(expectedContent, Files.readString(testFile));
320 | }
321 | }
322 |
323 | @Nested
324 | @DisplayName("Regex Support Tests")
325 | class RegexTests {
326 |
327 | @Test
328 | void shouldHandleSimpleFormatWithRegex() throws Exception {
329 | // Given
330 | String initialContent = "User123, User456, User789";
331 | Path testFile = tempDir.resolve("regex-simple-format.txt");
332 | Files.writeString(testFile, initialContent);
333 |
334 | // Simple text format with regex enabled
335 | String edits = "{\"oldText\":\"User[0-9]{3}\",\"newText\":\"Member\",\"useRegex\":\"true\"}";
336 |
337 | // When
338 | String result = editFileService.editFile(testFile.toString(), edits, false);
339 | JsonNode jsonResult = objectMapper.readTree(result);
340 |
341 | // Then
342 | assertTrue(jsonResult.get("success").asBoolean());
343 |
344 | // Verify only the first match was replaced
345 | String expectedContent = "Member, User456, User789";
346 | assertEquals(expectedContent, Files.readString(testFile));
347 | }
348 |
349 | @Test
350 | void shouldReplaceOnlyFirstRegexMatch() throws Exception {
351 | // Given
352 | String initialContent = "User123, User456, User789";
353 | Path testFile = tempDir.resolve("regex-basic.txt");
354 | Files.writeString(testFile, initialContent);
355 |
356 | // Use regex to match pattern - should only replace first occurrence
357 | String edits = "{\"oldText\":\"User\\\\d+\",\"newText\":\"Member\",\"useRegex\":\"true\"}";
358 |
359 | // When
360 | String result = editFileService.editFile(testFile.toString(), edits, false);
361 | JsonNode jsonResult = objectMapper.readTree(result);
362 |
363 | // Then
364 | assertTrue(jsonResult.get("success").asBoolean());
365 |
366 | // Verify only the first match was replaced, not all matches
367 | String expectedContent = "Member, User456, User789";
368 | assertEquals(expectedContent, Files.readString(testFile));
369 | }
370 |
371 | @Test
372 | void shouldHandleInvalidRegexPattern() throws Exception {
373 | // Given
374 | String initialContent = "Test invalid regex";
375 | Path testFile = tempDir.resolve("invalid-regex.txt");
376 | Files.writeString(testFile, initialContent);
377 |
378 | // Invalid regex pattern (unclosed parenthesis)
379 | String edits = "{\"oldText\":\"Test (invalid\",\"newText\":\"Fixed\",\"useRegex\":\"true\"}";
380 |
381 | // When
382 | String result = editFileService.editFile(testFile.toString(), edits, false);
383 | JsonNode jsonResult = objectMapper.readTree(result);
384 |
385 | // Then
386 | assertFalse(jsonResult.get("success").asBoolean());
387 | assertTrue(jsonResult.get("error").asText().contains("Invalid regex pattern"));
388 |
389 | // Verify file was not modified
390 | assertEquals(initialContent, Files.readString(testFile));
391 | }
392 | }
393 |
394 | @Nested
395 | @DisplayName("Edge Cases Tests")
396 | class EdgeCasesTests {
397 |
398 | @Test
399 | void shouldHandleEmptyFile() throws Exception {
400 | // Given
401 | String initialContent = "";
402 | Path testFile = tempDir.resolve("empty-file.txt");
403 | Files.writeString(testFile, initialContent);
404 |
405 | String edits = "{\"oldText\":\"\",\"newText\":\"Added content\"}";
406 |
407 | // When
408 | String result = editFileService.editFile(testFile.toString(), edits, false);
409 | JsonNode jsonResult = objectMapper.readTree(result);
410 |
411 | // Then
412 | assertTrue(jsonResult.get("success").asBoolean());
413 |
414 | // Verify content was added to empty file
415 | String expectedContent = "Added content";
416 | assertEquals(expectedContent, Files.readString(testFile));
417 | }
418 |
419 | @Test
420 | void shouldHandleLargeFile() throws Exception {
421 | // Given
422 | StringBuilder largeContent = new StringBuilder();
423 | for (int i = 0; i < 10000; i++) {
424 | largeContent.append("Line ").append(i).append("\n");
425 | }
426 |
427 | Path testFile = tempDir.resolve("large-file.txt");
428 | Files.writeString(testFile, largeContent.toString());
429 |
430 | // Find and replace a specific line
431 | String edits = "{\"oldText\":\"Line 5000\",\"newText\":\"MODIFIED LINE\"}";
432 |
433 | // When
434 | String result = editFileService.editFile(testFile.toString(), edits, false);
435 | JsonNode jsonResult = objectMapper.readTree(result);
436 |
437 | // Then
438 | assertTrue(jsonResult.get("success").asBoolean());
439 |
440 | // Verify the specific line was replaced
441 | assertTrue(Files.readString(testFile).contains("MODIFIED LINE"));
442 | }
443 |
444 | @Test
445 | void shouldHandleSpecialCharacters() throws Exception {
446 | // Given
447 | String initialContent = "Special chars: áéíóú ñÑ 你好 こんにちは";
448 | Path testFile = tempDir.resolve("special-chars.txt");
449 | Files.writeString(testFile, initialContent);
450 |
451 | String edits = "{\"oldText\":\"áéíóú ñÑ\",\"newText\":\"Unicode Works!\"}";
452 |
453 | // When
454 | String result = editFileService.editFile(testFile.toString(), edits, false);
455 | JsonNode jsonResult = objectMapper.readTree(result);
456 |
457 | // Then
458 | assertTrue(jsonResult.get("success").asBoolean());
459 |
460 | // Verify unicode characters were handled correctly
461 | String expectedContent = "Special chars: Unicode Works! 你好 こんにちは";
462 | assertEquals(expectedContent, Files.readString(testFile));
463 | }
464 | }
465 | }
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/tools/FetchWebpageServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.devoxx.mcp.filesystem.tools;
2 |
3 | import com.fasterxml.jackson.databind.JsonNode;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import org.jsoup.Connection;
6 | import org.jsoup.Jsoup;
7 | import org.jsoup.nodes.Document;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.extension.ExtendWith;
11 | import org.mockito.Mock;
12 | import org.mockito.MockedStatic;
13 | import org.mockito.Mockito;
14 | import org.mockito.junit.jupiter.MockitoExtension;
15 |
16 | import java.io.IOException;
17 |
18 | import static org.junit.jupiter.api.Assertions.*;
19 | import static org.mockito.ArgumentMatchers.anyInt;
20 | import static org.mockito.ArgumentMatchers.anyString;
21 | import static org.mockito.Mockito.when;
22 |
23 | @ExtendWith(MockitoExtension.class)
24 | class FetchWebpageServiceTest {
25 |
26 | private FetchWebpageService fetchWebpageService;
27 | private ObjectMapper objectMapper;
28 |
29 | @Mock
30 | private Connection connectionMock;
31 |
32 | @Mock
33 | private Document documentMock;
34 |
35 | @BeforeEach
36 | void setUp() {
37 | fetchWebpageService = new FetchWebpageService();
38 | objectMapper = new ObjectMapper();
39 | }
40 |
41 | @Test
42 | void shouldFetchWebpageSuccessfully() throws Exception {
43 | // Given
44 | String url = "https://example.com";
45 | String expectedTitle = "Example Domain";
46 | String expectedContent = "This domain is for use in illustrative examples";
47 |
48 | try (MockedStatic jsoupMock = Mockito.mockStatic(Jsoup.class)) {
49 | jsoupMock.when(() -> Jsoup.connect(anyString())).thenReturn(connectionMock);
50 | when(connectionMock.timeout(anyInt())).thenReturn(connectionMock);
51 | when(connectionMock.get()).thenReturn(documentMock);
52 | when(documentMock.title()).thenReturn(expectedTitle);
53 | when(documentMock.text()).thenReturn(expectedContent);
54 |
55 | // When
56 | String result = fetchWebpageService.fetchWebpage(url, 5000);
57 | JsonNode jsonResult = objectMapper.readTree(result);
58 |
59 | // Then
60 | assertTrue(jsonResult.get("success").asBoolean());
61 | assertEquals(url, jsonResult.get("url").asText());
62 | assertEquals(expectedTitle, jsonResult.get("title").asText());
63 | assertEquals(expectedContent, jsonResult.get("content").asText());
64 | }
65 | }
66 |
67 | @Test
68 | void shouldUseDefaultTimeoutWhenNotProvided() throws Exception {
69 | // Given
70 | String url = "https://example.com";
71 |
72 | try (MockedStatic jsoupMock = Mockito.mockStatic(Jsoup.class)) {
73 | jsoupMock.when(() -> Jsoup.connect(anyString())).thenReturn(connectionMock);
74 | // Verify that the default timeout (10000ms) is used when not specified
75 | when(connectionMock.timeout(10000)).thenReturn(connectionMock);
76 | when(connectionMock.get()).thenReturn(documentMock);
77 | when(documentMock.title()).thenReturn("Title");
78 | when(documentMock.text()).thenReturn("Content");
79 |
80 | // When
81 | String result = fetchWebpageService.fetchWebpage(url, null);
82 | JsonNode jsonResult = objectMapper.readTree(result);
83 |
84 | // Then
85 | assertTrue(jsonResult.get("success").asBoolean());
86 | }
87 | }
88 |
89 | @Test
90 | void shouldHandleIOException() throws Exception {
91 | // Given
92 | String url = "https://invalid-url.example";
93 |
94 | try (MockedStatic jsoupMock = Mockito.mockStatic(Jsoup.class)) {
95 | jsoupMock.when(() -> Jsoup.connect(anyString())).thenReturn(connectionMock);
96 | when(connectionMock.timeout(anyInt())).thenReturn(connectionMock);
97 | when(connectionMock.get()).thenThrow(new IOException("Connection refused"));
98 |
99 | // When
100 | String result = fetchWebpageService.fetchWebpage(url, 5000);
101 | JsonNode jsonResult = objectMapper.readTree(result);
102 |
103 | // Then
104 | assertFalse(jsonResult.get("success").asBoolean());
105 | assertTrue(jsonResult.get("error").asText().contains("Failed to access url"));
106 | }
107 | }
108 |
109 | @Test
110 | void shouldHandleUnexpectedException() throws Exception {
111 | // Given
112 | String url = "https://example.com";
113 |
114 | try (MockedStatic jsoupMock = Mockito.mockStatic(Jsoup.class)) {
115 | jsoupMock.when(() -> Jsoup.connect(anyString())).thenReturn(connectionMock);
116 | when(connectionMock.timeout(anyInt())).thenReturn(connectionMock);
117 | when(connectionMock.get()).thenThrow(new RuntimeException("Unexpected error"));
118 |
119 | // When
120 | String result = fetchWebpageService.fetchWebpage(url, 5000);
121 | JsonNode jsonResult = objectMapper.readTree(result);
122 |
123 | // Then
124 | assertFalse(jsonResult.get("success").asBoolean());
125 | assertTrue(jsonResult.get("error").asText().contains("Failed to access url"));
126 | }
127 | }
128 |
129 | @Test
130 | void shouldHandleNullUrl() throws Exception {
131 | // When
132 | String result = fetchWebpageService.fetchWebpage(null, 5000);
133 | JsonNode jsonResult = objectMapper.readTree(result);
134 |
135 | // Then
136 | assertFalse(jsonResult.get("success").asBoolean());
137 | assertTrue(jsonResult.get("error").asText().contains("Failed to access"));
138 | }
139 |
140 | @Test
141 | void shouldHandleEmptyUrl() throws Exception {
142 | // When
143 | String result = fetchWebpageService.fetchWebpage("", 5000);
144 | JsonNode jsonResult = objectMapper.readTree(result);
145 |
146 | // Then
147 | assertFalse(jsonResult.get("success").asBoolean());
148 | assertTrue(jsonResult.get("error").asText().contains("Failed"));
149 | }
150 | }
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/tools/GrepFilesServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.devoxx.mcp.filesystem.tools;
2 |
3 | import com.fasterxml.jackson.databind.JsonNode;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.Nested;
8 | import org.junit.jupiter.api.DisplayName;
9 | import org.junit.jupiter.api.io.TempDir;
10 |
11 | import java.nio.file.Files;
12 | import java.nio.file.Path;
13 |
14 | import static org.junit.jupiter.api.Assertions.*;
15 |
16 | class GrepFilesServiceTest {
17 |
18 | private GrepFilesService grepFilesService;
19 | private ObjectMapper objectMapper;
20 |
21 | @TempDir
22 | Path tempDir;
23 |
24 | @BeforeEach
25 | void setUp() {
26 | grepFilesService = new GrepFilesService();
27 | objectMapper = new ObjectMapper();
28 | }
29 |
30 | private void createTestFiles(Path directory) throws Exception {
31 | // Create a few test files with different content and extensions
32 | Path textFile1 = directory.resolve("file1.txt");
33 | Files.writeString(textFile1, "This is a sample text file\nWith multiple lines\nAnd some important text here\nline 1\nline 2");
34 |
35 | Path textFile2 = directory.resolve("file2.txt");
36 | Files.writeString(textFile2, "Another sample file\nWith different content\nBut no important keywords");
37 |
38 | Path javaFile = directory.resolve("Example.java");
39 | Files.writeString(javaFile, """
40 | public class Example {
41 | // Sample Java code
42 | public static void main(String[] args) {
43 | System.out.println("Hello, World!");
44 | // line 10 of code
45 | }
46 | }
47 | """);
48 |
49 | Path xmlFile = directory.resolve("config.xml");
50 | Files.writeString(xmlFile, """
51 |
52 | value
53 | setting
54 |
55 | """);
56 | }
57 |
58 | @Nested
59 | @DisplayName("Basic Search Tests")
60 | class BasicSearchTests {
61 |
62 | @Test
63 | void shouldFindSimpleTextPattern() throws Exception {
64 | // Given
65 | createTestFiles(tempDir);
66 | String pattern = "important text";
67 |
68 | // When
69 | String result = grepFilesService.grepFiles(tempDir.toString(), pattern, null, false, null, null);
70 | JsonNode jsonResult = objectMapper.readTree(result);
71 |
72 | // Then
73 | assertTrue(jsonResult.get("success").asBoolean());
74 | assertTrue(jsonResult.get("results").size() > 0);
75 | }
76 |
77 | @Test
78 | void shouldRespectFileExtensionFilter() throws Exception {
79 | // Given
80 | createTestFiles(tempDir);
81 | String pattern = "sample";
82 |
83 | // When - search only in .txt files
84 | String result = grepFilesService.grepFiles(tempDir.toString(), pattern, ".txt", false, null, null);
85 | JsonNode jsonResult = objectMapper.readTree(result);
86 |
87 | // Then
88 | assertTrue(jsonResult.get("success").asBoolean());
89 | assertTrue(!jsonResult.get("results").isEmpty());
90 |
91 | // All matches should be in .txt files
92 | for (JsonNode matchText : jsonResult.get("results")) {
93 | if (matchText.asText().contains("(") && matchText.asText().contains("matches)")) {
94 | assertTrue(matchText.asText().contains(".txt"));
95 | }
96 | }
97 | }
98 | }
99 |
100 | @Nested
101 | @DisplayName("Filter Tests")
102 | class FilterTests {
103 |
104 | @Test
105 | void shouldSearchAllFilesWhenExtensionFilterIsNull() throws Exception {
106 | // Given
107 | createTestFiles(tempDir);
108 | String pattern = "sample"; // Appears in both .txt and .xml files
109 |
110 | // When - extension filter is null
111 | String resultWithNullFilter = grepFilesService.grepFiles(tempDir.toString(), pattern, null, false, null, null);
112 | JsonNode jsonResultNullFilter = objectMapper.readTree(resultWithNullFilter);
113 |
114 | // When - extension filter is empty
115 | String resultWithEmptyFilter = grepFilesService.grepFiles(tempDir.toString(), pattern, "", false, null, null);
116 | JsonNode jsonResultEmptyFilter = objectMapper.readTree(resultWithEmptyFilter);
117 |
118 | // Then - both should search all files
119 | assertTrue(jsonResultNullFilter.get("success").asBoolean());
120 | assertTrue(jsonResultEmptyFilter.get("success").asBoolean());
121 |
122 | // The summary should have the same number of matches for both null and empty filter
123 | String summaryNull = jsonResultNullFilter.get("summary").asText();
124 | String summaryEmpty = jsonResultEmptyFilter.get("summary").asText();
125 | assertTrue(summaryNull.contains("Found") && summaryEmpty.contains("Found"));
126 | assertEquals(summaryNull, summaryEmpty);
127 |
128 | // We should have matches in files with different extensions
129 | boolean foundTxtFile = false;
130 | boolean foundXmlFile = false;
131 |
132 | for (JsonNode matchText : jsonResultNullFilter.get("results")) {
133 | String text = matchText.asText();
134 | if (text.contains(".txt")) foundTxtFile = true;
135 | if (text.contains(".xml")) foundXmlFile = true;
136 | }
137 |
138 | assertTrue(foundTxtFile || foundXmlFile, "Should find matches in different file types");
139 | }
140 | }
141 |
142 | @Nested
143 | @DisplayName("Advanced Search Tests")
144 | class AdvancedSearchTests {
145 |
146 | @Test
147 | void shouldUseRegexPatternMatching() throws Exception {
148 | // Given
149 | createTestFiles(tempDir);
150 | // Regex to match "line" followed by a number
151 | String pattern = "line\\s+[0-9]+";
152 |
153 | // When
154 | String result = grepFilesService.grepFiles(tempDir.toString(), pattern, null, true, null, null);
155 | JsonNode jsonResult = objectMapper.readTree(result);
156 |
157 | // Then
158 | assertTrue(jsonResult.get("success").asBoolean());
159 | assertTrue(jsonResult.get("results").size() > 0);
160 | }
161 |
162 | @Test
163 | void shouldProvideContextLines() throws Exception {
164 | // Given
165 | createTestFiles(tempDir);
166 | String pattern = "important text";
167 | int contextLines = 2;
168 |
169 | // When
170 | String result = grepFilesService.grepFiles(tempDir.toString(), pattern, null, false, contextLines, null);
171 | JsonNode jsonResult = objectMapper.readTree(result);
172 |
173 | // Then
174 | assertTrue(jsonResult.get("success").asBoolean());
175 |
176 | // Check that context lines are included
177 | // The context is included in the results as text with line breaks
178 | boolean foundContext = false;
179 | for (JsonNode aResult : jsonResult.get("results")) {
180 | String text = aResult.asText();
181 | if (text.contains("→") && text.contains("important text")) {
182 | foundContext = true;
183 | break;
184 | }
185 | }
186 | assertTrue(foundContext, "Should include context lines with arrow symbol");
187 | }
188 |
189 | }
190 |
191 | @Nested
192 | @DisplayName("Error Handling Tests")
193 | class ErrorHandlingTests {
194 |
195 | @Test
196 | void shouldHandleNonExistentDirectory() throws Exception {
197 | // Given
198 | String nonExistentDir = tempDir.resolve("non-existent").toString();
199 | String pattern = "test";
200 |
201 | // When
202 | String result = grepFilesService.grepFiles(nonExistentDir, pattern, null, false, null, null);
203 | JsonNode jsonResult = objectMapper.readTree(result);
204 |
205 | // Then
206 | assertFalse(jsonResult.get("success").asBoolean());
207 | assertTrue(jsonResult.has("error"));
208 | assertTrue(jsonResult.get("error").asText().contains("Directory does not exist"));
209 | }
210 |
211 | @Test
212 | void shouldHandleInvalidPattern() throws Exception {
213 | // Given
214 | createTestFiles(tempDir);
215 | String invalidRegexPattern = "[";
216 |
217 | // When - trying to use the invalid pattern as regex
218 | String result = grepFilesService.grepFiles(tempDir.toString(), invalidRegexPattern, null, true, null, null);
219 | JsonNode jsonResult = objectMapper.readTree(result);
220 |
221 | // Then
222 | assertFalse(jsonResult.get("success").asBoolean());
223 | assertTrue(jsonResult.has("error"));
224 | }
225 | }
226 |
227 | // TODO Make a test with the above grep search arguments because it should return a result
228 | @Test
229 | void basicGrepSearch() throws Exception {
230 | createTestFiles(tempDir);
231 |
232 | GrepFilesService grepFilesService = new GrepFilesService();
233 | String result = grepFilesService.grepFiles(tempDir.toString(), "sample", ".java", false, null, 5);
234 |
235 | JsonNode jsonResult = objectMapper.readTree(result);
236 |
237 | String summary = jsonResult.get("summary").asText();
238 |
239 | assertTrue(summary.contains("Found 1 matches in 1 files"),
240 | "Summary should mention 1 found file");
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/src/test/java/com/devoxx/mcp/filesystem/tools/ListDirectoryServiceTest.java:
--------------------------------------------------------------------------------
1 | package com.devoxx.mcp.filesystem.tools;
2 |
3 | import com.fasterxml.jackson.databind.JsonNode;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import org.junit.jupiter.api.BeforeEach;
6 | import org.junit.jupiter.api.Test;
7 | import org.junit.jupiter.api.io.TempDir;
8 |
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.Map;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 |
17 | class ListDirectoryServiceTest {
18 |
19 | private ListDirectoryService listDirectoryService;
20 | private ObjectMapper objectMapper;
21 |
22 | @TempDir
23 | Path tempDir;
24 |
25 | @BeforeEach
26 | void setUp() {
27 | listDirectoryService = new ListDirectoryService();
28 | objectMapper = new ObjectMapper();
29 | }
30 |
31 | @Test
32 | void shouldListDirectoryContents() throws Exception {
33 | // Given
34 | // Create a test directory structure
35 | Path file1 = tempDir.resolve("file1.txt");
36 | Path file2 = tempDir.resolve("file2.log");
37 | Path subDir = tempDir.resolve("subdir");
38 |
39 | Files.writeString(file1, "Content of file 1");
40 | Files.writeString(file2, "Content of file 2");
41 | Files.createDirectory(subDir);
42 |
43 | // When
44 | String result = listDirectoryService.listDirectory(tempDir.toString());
45 | JsonNode jsonResult = objectMapper.readTree(result);
46 |
47 | // Then
48 | assertTrue(jsonResult.get("success").asBoolean());
49 | assertEquals(tempDir.toString(), jsonResult.get("path").asText());
50 | assertEquals(3, jsonResult.get("count").asInt());
51 |
52 | // Verify entries
53 | JsonNode entries = jsonResult.get("entries");
54 | assertTrue(entries.isArray());
55 |
56 | // Collect entry names and types for easier verification
57 | List