Skip to content

Commit 4bcaf22

Browse files
committed
Introduce integration tests
1 parent b19d9f1 commit 4bcaf22

File tree

5 files changed

+1470
-0
lines changed

5 files changed

+1470
-0
lines changed
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package integration
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.engine.cio.CIO
5+
import io.ktor.client.plugins.sse.SSE
6+
import io.ktor.server.application.install
7+
import io.ktor.server.engine.EmbeddedServer
8+
import io.ktor.server.cio.CIO as ServerCIO
9+
import io.ktor.server.engine.embeddedServer
10+
import io.ktor.server.routing.routing
11+
import io.ktor.server.sse.SSE as ServerSSE
12+
import io.modelcontextprotocol.kotlin.sdk.Implementation
13+
import io.modelcontextprotocol.kotlin.sdk.ListResourcesRequest
14+
import io.modelcontextprotocol.kotlin.sdk.ServerCapabilities
15+
import io.modelcontextprotocol.kotlin.sdk.client.Client
16+
import io.modelcontextprotocol.kotlin.sdk.client.mcpSse
17+
import io.modelcontextprotocol.kotlin.sdk.server.Server
18+
import io.modelcontextprotocol.kotlin.sdk.server.ServerOptions
19+
import io.modelcontextprotocol.kotlin.sdk.server.mcp
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.runBlocking
22+
import kotlinx.coroutines.withContext
23+
import kotlinx.coroutines.withTimeout
24+
import org.junit.jupiter.api.AfterEach
25+
import org.junit.jupiter.api.BeforeEach
26+
import org.junit.jupiter.api.Test
27+
import org.junit.jupiter.api.assertThrows
28+
import kotlin.test.assertEquals
29+
import kotlin.test.assertNotNull
30+
import kotlin.test.assertTrue
31+
import kotlin.time.Duration.Companion.seconds
32+
33+
/**
34+
* These tests verify that the client can connect to the server and perform basic operations.
35+
*/
36+
class BasicConnectivityTest {
37+
38+
private val port = 3003
39+
private val host = "localhost"
40+
41+
private lateinit var server: Server
42+
private lateinit var client: Client
43+
private lateinit var serverEngine: EmbeddedServer<*, *>
44+
45+
@BeforeEach
46+
fun setUp() {
47+
val capabilities = ServerCapabilities(
48+
resources = ServerCapabilities.Resources(
49+
subscribe = true,
50+
listChanged = true,
51+
),
52+
tools = ServerCapabilities.Tools(
53+
listChanged = true
54+
)
55+
)
56+
57+
server = Server(
58+
Implementation(name = "test-server", version = "1.0"),
59+
ServerOptions(capabilities = capabilities)
60+
)
61+
62+
serverEngine = embeddedServer(ServerCIO, host = host, port = port) {
63+
install(ServerSSE)
64+
routing {
65+
mcp { server }
66+
}
67+
}.start(wait = false)
68+
}
69+
70+
@AfterEach
71+
fun tearDown() {
72+
if (::client.isInitialized) {
73+
try {
74+
runBlocking {
75+
withTimeout(3.seconds) {
76+
client.close()
77+
}
78+
}
79+
} catch (e: Exception) {
80+
println("Warning: Error during client close: ${e.message}")
81+
}
82+
}
83+
84+
if (::serverEngine.isInitialized) {
85+
try {
86+
serverEngine.stop(500, 1000)
87+
} catch (e: Exception) {
88+
println("Warning: Error during server stop: ${e.message}")
89+
}
90+
}
91+
}
92+
93+
@Test
94+
fun testClientConnectsToServer() {
95+
runBlocking {
96+
withContext(Dispatchers.IO) {
97+
client = HttpClient(CIO) {
98+
install(SSE)
99+
}.mcpSse("http://$host:$port")
100+
101+
assertNotNull(client, "Client should be initialized")
102+
}
103+
}
104+
}
105+
106+
@Test
107+
fun testPing() {
108+
runBlocking {
109+
withContext(Dispatchers.IO) {
110+
client = HttpClient(CIO) {
111+
install(SSE)
112+
}.mcpSse("http://$host:$port")
113+
114+
val result = client.ping()
115+
116+
assertNotNull(result, "Ping result should not be null")
117+
}
118+
}
119+
}
120+
121+
@Test
122+
fun testMinimalServerCapabilities() {
123+
if (::serverEngine.isInitialized) {
124+
serverEngine.stop(500, 1000)
125+
}
126+
127+
val minimalCapabilities = ServerCapabilities()
128+
129+
server = Server(
130+
Implementation(name = "minimal-server", version = "1.0.0"),
131+
ServerOptions(capabilities = minimalCapabilities)
132+
)
133+
134+
serverEngine = embeddedServer(ServerCIO, host = host, port = port) {
135+
install(ServerSSE)
136+
routing {
137+
mcp { server }
138+
}
139+
}.start(wait = false)
140+
141+
runBlocking {
142+
withContext(Dispatchers.IO) {
143+
client = HttpClient(CIO) {
144+
install(SSE)
145+
}.mcpSse("http://$host:$port")
146+
147+
assertNotNull(client, "Client should be initialized")
148+
149+
val pingResult = client.ping()
150+
assertNotNull(pingResult, "Ping result should not be null")
151+
152+
val serverCapabilities = client.serverCapabilities
153+
assertNotNull(serverCapabilities, "Server capabilities should not be null")
154+
assertEquals(null, serverCapabilities.resources?.listChanged,
155+
"Resources listChanged capability should be null")
156+
assertEquals(null, serverCapabilities.tools?.listChanged,
157+
"Tools listChanged capability should be null")
158+
}
159+
}
160+
}
161+
162+
@Test
163+
fun testServerCapabilitiesVerification() {
164+
runBlocking {
165+
withContext(Dispatchers.IO) {
166+
client = HttpClient(CIO) {
167+
install(SSE)
168+
}.mcpSse("http://$host:$port")
169+
170+
assertNotNull(client, "Client should be initialized")
171+
172+
val serverCapabilities = client.serverCapabilities
173+
assertNotNull(serverCapabilities, "Server capabilities should not be null")
174+
175+
assertNotNull(serverCapabilities.resources, "Resources capabilities should not be null")
176+
assertEquals(true, serverCapabilities.resources.subscribe,
177+
"Resources subscribe capability should be true")
178+
assertEquals(true, serverCapabilities.resources.listChanged,
179+
"Resources listChanged capability should be true")
180+
181+
assertNotNull(serverCapabilities.tools, "Tools capabilities should not be null")
182+
assertEquals(true, serverCapabilities.tools.listChanged,
183+
"Tools listChanged capability should be true")
184+
}
185+
}
186+
}
187+
188+
@Test
189+
fun testServerVersionCompatibility() {
190+
if (::serverEngine.isInitialized) {
191+
serverEngine.stop(500, 1000)
192+
}
193+
194+
val capabilities = ServerCapabilities(
195+
resources = ServerCapabilities.Resources(
196+
subscribe = true,
197+
listChanged = true,
198+
),
199+
tools = ServerCapabilities.Tools(
200+
listChanged = true
201+
)
202+
)
203+
204+
server = Server(
205+
Implementation(name = "test-server", version = "2.0.0"),
206+
ServerOptions(capabilities = capabilities)
207+
)
208+
209+
serverEngine = embeddedServer(ServerCIO, host = host, port = port) {
210+
install(ServerSSE)
211+
routing {
212+
mcp { server }
213+
}
214+
}.start(wait = false)
215+
216+
runBlocking {
217+
withContext(Dispatchers.IO) {
218+
client = HttpClient(CIO) {
219+
install(SSE)
220+
}.mcpSse("http://$host:$port")
221+
222+
assertNotNull(client, "Client should be initialized")
223+
224+
val serverImpl = client.serverVersion
225+
assertNotNull(serverImpl, "Server implementation should not be null")
226+
assertEquals("test-server", serverImpl.name, "Server name should match")
227+
assertEquals("2.0.0", serverImpl.version, "Server version should match")
228+
229+
val pingResult = client.ping()
230+
assertNotNull(pingResult, "Ping result should not be null")
231+
}
232+
}
233+
}
234+
235+
@Test
236+
fun testConnectionErrorHandling() {
237+
val invalidPort = 9999
238+
239+
runBlocking {
240+
withContext(Dispatchers.IO) {
241+
// Attempt to connect to a non-existent server
242+
val exception = assertThrows<IllegalStateException> {
243+
HttpClient(CIO) {
244+
install(SSE)
245+
}.mcpSse("http://$host:$invalidPort")
246+
}
247+
248+
assertTrue(exception.message?.contains("Error connecting to transport") == true,
249+
"Exception should indicate connection error")
250+
}
251+
}
252+
}
253+
}

0 commit comments

Comments
 (0)