Skip to content

Commit 42e645f

Browse files
committed
build: unit test for modules: mcp/vertx-connection pool
1 parent 2c97882 commit 42e645f

13 files changed

Lines changed: 1497 additions & 19 deletions

File tree

modules/jooby-mcp/pom.xml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,43 @@
3030
<optional>true</optional>
3131
</dependency>
3232

33+
<!-- Test dependencies -->
34+
<dependency>
35+
<groupId>org.junit.jupiter</groupId>
36+
<artifactId>junit-jupiter-engine</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
40+
<dependency>
41+
<groupId>org.junit.jupiter</groupId>
42+
<artifactId>junit-jupiter-params</artifactId>
43+
<scope>test</scope>
44+
</dependency>
45+
46+
<dependency>
47+
<groupId>org.assertj</groupId>
48+
<artifactId>assertj-core</artifactId>
49+
<scope>test</scope>
50+
</dependency>
51+
52+
<dependency>
53+
<groupId>org.jacoco</groupId>
54+
<artifactId>org.jacoco.agent</artifactId>
55+
<classifier>runtime</classifier>
56+
<scope>test</scope>
57+
</dependency>
58+
59+
<dependency>
60+
<groupId>org.mockito</groupId>
61+
<artifactId>mockito-core</artifactId>
62+
<scope>test</scope>
63+
</dependency>
64+
65+
<dependency>
66+
<groupId>org.mockito</groupId>
67+
<artifactId>mockito-junit-jupiter</artifactId>
68+
<scope>test</scope>
69+
</dependency>
3370
</dependencies>
3471

3572
<build>

modules/jooby-mcp/src/main/java/io/jooby/mcp/instrumentation/OtelMcpTracing.java

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
*/
66
package io.jooby.mcp.instrumentation;
77

8-
import java.util.List;
9-
108
import org.jspecify.annotations.NonNull;
119
import org.jspecify.annotations.Nullable;
1210

@@ -122,21 +120,4 @@ private static void traceError(Throwable cause, Span span) {
122120
span.setAttribute("error.type", cause.getClass().getName());
123121
}
124122
}
125-
126-
private String extractErrorMessage(List<McpSchema.Content> contentList) {
127-
if (contentList == null || contentList.isEmpty()) {
128-
return "Tool execution failed (no content provided)";
129-
}
130-
131-
McpSchema.Content first = contentList.getFirst();
132-
133-
return switch (first) {
134-
case McpSchema.TextContent text -> text.text();
135-
case McpSchema.ImageContent img -> "[Image: " + img.mimeType() + "]";
136-
case McpSchema.AudioContent audio -> "[Audio]";
137-
case McpSchema.EmbeddedResource embedded ->
138-
"[Embedded Resource: " + embedded.resource().uri() + "]";
139-
case McpSchema.ResourceLink link -> "[Resource Link: " + link.uri() + "]";
140-
};
141-
}
142123
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.mcp;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
import static org.mockito.Mockito.*;
10+
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
14+
import io.jooby.Jooby;
15+
import io.jooby.Router;
16+
import io.jooby.StatusCode;
17+
import io.jooby.mcp.McpChain;
18+
import io.jooby.mcp.McpOperation;
19+
import io.modelcontextprotocol.common.McpTransportContext;
20+
import io.modelcontextprotocol.spec.McpError;
21+
import io.modelcontextprotocol.spec.McpSchema;
22+
23+
public class McpExecutorTest {
24+
25+
private Jooby app;
26+
private Router router;
27+
private McpExecutor executor;
28+
private McpTransportContext transportContext;
29+
private McpChain chain;
30+
private McpOperation operation;
31+
32+
@BeforeEach
33+
void setUp() {
34+
app = mock(Jooby.class);
35+
router = mock(Router.class);
36+
when(app.getRouter()).thenReturn(router);
37+
38+
executor = new McpExecutor(app);
39+
transportContext = mock(McpTransportContext.class);
40+
chain = mock(McpChain.class);
41+
operation = mock(McpOperation.class);
42+
43+
// Setup default operation behavior
44+
when(operation.getClassName()).thenReturn(getClass().getName());
45+
when(operation.getId()).thenReturn("test-op");
46+
47+
// Global router default to prevent NPE during logging
48+
when(router.errorCode(any())).thenReturn(StatusCode.SERVER_ERROR);
49+
}
50+
51+
@Test
52+
void testInvokeSuccess() throws Throwable {
53+
Object result = new Object();
54+
when(chain.proceed(any(), eq(transportContext), eq(operation))).thenReturn(result);
55+
56+
Object actual = executor.invoke(null, transportContext, operation, chain);
57+
58+
assertEquals(result, actual);
59+
}
60+
61+
@Test
62+
void testInvokeFatalError() throws Throwable {
63+
// OutOfMemoryError triggers SneakyThrows.isFatal
64+
OutOfMemoryError fatal = new OutOfMemoryError();
65+
when(chain.proceed(any(), any(), any())).thenThrow(fatal);
66+
67+
// This should now propagate the fatal error after logging
68+
assertThrows(
69+
OutOfMemoryError.class, () -> executor.invoke(null, transportContext, operation, chain));
70+
71+
verify(operation).exception(fatal);
72+
}
73+
74+
@Test
75+
void testInvokeToolError() throws Throwable {
76+
Exception error = new Exception("tool-failure");
77+
when(chain.proceed(any(), any(), any())).thenThrow(error);
78+
when(operation.isTool()).thenReturn(true);
79+
80+
Object result = executor.invoke(null, transportContext, operation, chain);
81+
82+
assertTrue(result instanceof McpSchema.CallToolResult);
83+
McpSchema.CallToolResult toolResult = (McpSchema.CallToolResult) result;
84+
assertTrue(toolResult.isError());
85+
assertEquals("tool-failure", ((McpSchema.TextContent) toolResult.content().get(0)).text());
86+
}
87+
88+
@Test
89+
void testInvokeMcpErrorRethrow() throws Throwable {
90+
McpSchema.JSONRPCResponse.JSONRPCError jsonError =
91+
new McpSchema.JSONRPCResponse.JSONRPCError(-32000, "mcp-error", null);
92+
McpError mcpError = new McpError(jsonError);
93+
94+
when(chain.proceed(any(), any(), any())).thenThrow(mcpError);
95+
96+
// Should rethrow the same McpError
97+
McpError actual =
98+
assertThrows(
99+
McpError.class, () -> executor.invoke(null, transportContext, operation, chain));
100+
101+
assertEquals(jsonError, actual.getJsonRpcError());
102+
}
103+
104+
@Test
105+
void testStatusCodeMapping() throws Throwable {
106+
checkMapping(new Exception(), StatusCode.NOT_FOUND, McpSchema.ErrorCodes.RESOURCE_NOT_FOUND);
107+
checkMapping(new Exception(), StatusCode.BAD_REQUEST, McpSchema.ErrorCodes.INVALID_PARAMS);
108+
checkMapping(new Exception(), StatusCode.CONFLICT, McpSchema.ErrorCodes.INVALID_PARAMS);
109+
checkMapping(new Exception(), StatusCode.FORBIDDEN, McpSchema.ErrorCodes.INTERNAL_ERROR);
110+
}
111+
112+
@Test
113+
void testIsServerErrorBranching() {
114+
assertTrue(McpExecutor.isServerError(McpSchema.ErrorCodes.INTERNAL_ERROR));
115+
assertTrue(McpExecutor.isServerError(-32701));
116+
assertFalse(McpExecutor.isServerError(-32600));
117+
}
118+
119+
@Test
120+
void testToolErrorWithNullMessage() throws Throwable {
121+
Exception error = new Exception((String) null);
122+
when(chain.proceed(any(), any(), any())).thenThrow(error);
123+
when(operation.isTool()).thenReturn(true);
124+
125+
Object result = executor.invoke(null, transportContext, operation, chain);
126+
McpSchema.CallToolResult toolResult = (McpSchema.CallToolResult) result;
127+
assertEquals(
128+
"Unknown error occurred", ((McpSchema.TextContent) toolResult.content().get(0)).text());
129+
}
130+
131+
private void checkMapping(Throwable t, StatusCode joobyCode, int expectedMcpCode)
132+
throws Throwable {
133+
reset(chain, router);
134+
when(chain.proceed(any(), any(), any())).thenThrow(t);
135+
when(router.errorCode(t)).thenReturn(joobyCode);
136+
137+
McpError ex =
138+
assertThrows(
139+
McpError.class, () -> executor.invoke(null, transportContext, operation, chain));
140+
assertEquals(expectedMcpCode, ex.getJsonRpcError().code());
141+
}
142+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.mcp;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
10+
import java.util.Map;
11+
12+
import org.junit.jupiter.api.Test;
13+
14+
import com.typesafe.config.Config;
15+
import com.typesafe.config.ConfigFactory;
16+
import io.jooby.exception.StartupException;
17+
import io.jooby.mcp.McpModule;
18+
19+
public class McpServerConfigTest {
20+
21+
@Test
22+
public void testAccessors() {
23+
McpServerConfig config = new McpServerConfig("my-server", "1.0");
24+
25+
config.setName("new-name");
26+
assertEquals("new-name", config.getName());
27+
28+
config.setVersion("2.0");
29+
assertEquals("2.0", config.getVersion());
30+
31+
config.setTransport(McpModule.Transport.SSE);
32+
assertEquals(McpModule.Transport.SSE, config.getTransport());
33+
assertTrue(config.isSseTransport());
34+
35+
config.setSseEndpoint("/sse");
36+
assertEquals("/sse", config.getSseEndpoint());
37+
38+
config.setMessageEndpoint("/msg");
39+
assertEquals("/msg", config.getMessageEndpoint());
40+
41+
config.setMcpEndpoint("/mcp-custom");
42+
assertEquals("/mcp-custom", config.getMcpEndpoint());
43+
44+
config.setDisallowDelete(true);
45+
assertTrue(config.isDisallowDelete());
46+
47+
config.setKeepAliveInterval(60);
48+
assertEquals(60, config.getKeepAliveInterval());
49+
50+
config.setInstructions("be helpful");
51+
assertEquals("be helpful", config.getInstructions());
52+
}
53+
54+
@Test
55+
public void testFromConfigWithDefaults() {
56+
Config config =
57+
ConfigFactory.parseMap(
58+
Map.of(
59+
"name", "test-server",
60+
"version", "0.1"));
61+
62+
McpServerConfig serverConfig = McpServerConfig.fromConfig("mcp", config);
63+
64+
assertEquals("test-server", serverConfig.getName());
65+
assertEquals("0.1", serverConfig.getVersion());
66+
// Default transport
67+
assertEquals(McpModule.Transport.STREAMABLE_HTTP, serverConfig.getTransport());
68+
assertFalse(serverConfig.isSseTransport());
69+
// Default endpoints
70+
assertEquals(McpServerConfig.DEFAULT_SSE_ENDPOINT, serverConfig.getSseEndpoint());
71+
assertEquals(McpServerConfig.DEFAULT_MESSAGE_ENDPOINT, serverConfig.getMessageEndpoint());
72+
assertEquals(McpServerConfig.DEFAULT_MCP_ENDPOINT, serverConfig.getMcpEndpoint());
73+
// Default booleans/nulls
74+
assertFalse(serverConfig.isDisallowDelete());
75+
assertNull(serverConfig.getKeepAliveInterval());
76+
assertNull(serverConfig.getInstructions());
77+
}
78+
79+
@Test
80+
public void testFromConfigFull() {
81+
Config config =
82+
ConfigFactory.parseMap(
83+
Map.of(
84+
"name", "full-server",
85+
"version", "1.0",
86+
"transport", "sse",
87+
"sseEndpoint", "/custom/sse",
88+
"messageEndpoint", "/custom/msg",
89+
"mcpEndpoint", "/custom/mcp",
90+
"instructions", "custom instructions",
91+
"disallowDelete", true,
92+
"keepAliveInterval", 30));
93+
94+
McpServerConfig serverConfig = McpServerConfig.fromConfig("mcp", config);
95+
96+
assertEquals(McpModule.Transport.SSE, serverConfig.getTransport());
97+
assertTrue(serverConfig.isSseTransport());
98+
assertEquals("/custom/sse", serverConfig.getSseEndpoint());
99+
assertEquals("/custom/msg", serverConfig.getMessageEndpoint());
100+
assertEquals("/custom/mcp", serverConfig.getMcpEndpoint());
101+
assertEquals("custom instructions", serverConfig.getInstructions());
102+
assertTrue(serverConfig.isDisallowDelete());
103+
assertEquals(30, serverConfig.getKeepAliveInterval());
104+
}
105+
106+
@Test
107+
public void testMissingRequiredName() {
108+
Config config = ConfigFactory.parseMap(Map.of("version", "1.0"));
109+
assertThrows(StartupException.class, () -> McpServerConfig.fromConfig("mcp", config));
110+
}
111+
112+
@Test
113+
public void testMissingRequiredVersion() {
114+
Config config = ConfigFactory.parseMap(Map.of("name", "server"));
115+
assertThrows(StartupException.class, () -> McpServerConfig.fromConfig("mcp", config));
116+
}
117+
}

0 commit comments

Comments
 (0)