diff --git a/build.gradle.kts b/build.gradle.kts index 09e9adb..f4ddd44 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,7 +61,7 @@ kover { } verify { rule { - minBound(65) + minBound(73) } } } diff --git a/kotlin-sdk-core/build.gradle.kts b/kotlin-sdk-core/build.gradle.kts index 40848b1..0e702c4 100644 --- a/kotlin-sdk-core/build.gradle.kts +++ b/kotlin-sdk-core/build.gradle.kts @@ -121,10 +121,8 @@ kotlin { commonTest { dependencies { + implementation(project(":test-utils")) implementation(kotlin("test")) - implementation(libs.kotlinx.coroutines.test) - implementation(libs.kotest.assertions.core) - implementation(libs.kotest.assertions.json) } } diff --git a/kotlin-sdk-core/detekt-baseline-commonTestSourceSet.xml b/kotlin-sdk-core/detekt-baseline-commonTestSourceSet.xml deleted file mode 100644 index 0a4c838..0000000 --- a/kotlin-sdk-core/detekt-baseline-commonTestSourceSet.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - EmptyFunctionBlock:ProtocolTest.kt:RecordingTransport${} - EmptyFunctionBlock:ProtocolTest.kt:TestProtocol${} - LongMethod:ElicitationTest.kt:ElicitationTest$@Test fun `should serialize ElicitRequest with requested schema` - LongMethod:SamplingDslTest.kt:SamplingDslTest$@Test fun `buildCreateMessageRequest should build with all fields` - LongMethod:SamplingTest.kt:SamplingTest$@Test fun `should serialize CreateMessageRequest with all fields` - LongMethod:ToolsTest.kt:ToolsTest$@Test fun `should serialize Tool with annotations and schemas` - - diff --git a/kotlin-sdk-core/detekt-baseline-test.xml b/kotlin-sdk-core/detekt-baseline-test.xml deleted file mode 100644 index 0a4c838..0000000 --- a/kotlin-sdk-core/detekt-baseline-test.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - EmptyFunctionBlock:ProtocolTest.kt:RecordingTransport${} - EmptyFunctionBlock:ProtocolTest.kt:TestProtocol${} - LongMethod:ElicitationTest.kt:ElicitationTest$@Test fun `should serialize ElicitRequest with requested schema` - LongMethod:SamplingDslTest.kt:SamplingDslTest$@Test fun `buildCreateMessageRequest should build with all fields` - LongMethod:SamplingTest.kt:SamplingTest$@Test fun `should serialize CreateMessageRequest with all fields` - LongMethod:ToolsTest.kt:ToolsTest$@Test fun `should serialize Tool with annotations and schemas` - - diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt index 142c2af..b228071 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/shared/ProtocolTest.kt @@ -154,9 +154,15 @@ class ProtocolTest { } private class TestProtocol : Protocol(null) { - override fun assertCapabilityForMethod(method: Method) {} - override fun assertNotificationCapability(method: Method) {} - override fun assertRequestHandlerCapability(method: Method) {} + override fun assertCapabilityForMethod(method: Method) { + // noop + } + override fun assertNotificationCapability(method: Method) { + // noop + } + override fun assertRequestHandlerCapability(method: Method) { + // noop + } } private class RecordingTransport : Transport { @@ -164,7 +170,9 @@ private class RecordingTransport : Transport { private var onMessageCallback: (suspend (JSONRPCMessage) -> Unit)? = null private var onCloseCallback: (() -> Unit)? = null - override suspend fun start() {} + override suspend fun start() { + // noop + } override suspend fun send(message: JSONRPCMessage, options: TransportSendOptions?) { sentMessages.send(message) @@ -178,7 +186,9 @@ private class RecordingTransport : Transport { onCloseCallback = block } - override fun onError(block: (Throwable) -> Unit) {} + override fun onError(block: (Throwable) -> Unit) { + // noop + } override fun onMessage(block: suspend (JSONRPCMessage) -> Unit) { onMessageCallback = block diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt index c288cca..33d5653 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CapabilitiesTest.kt @@ -1,6 +1,9 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerializationRoundTrip import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlin.test.Test @@ -15,14 +18,16 @@ class CapabilitiesTest { name = "test-server", version = "1.0.0", ) - val json = McpJson.encodeToString(implementation) - - json shouldEqualJson """ + verifySerialization( + implementation, + McpJson, + """ { "name": "test-server", "version": "1.0.0" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -36,9 +41,10 @@ class CapabilitiesTest { Icon(src = "https://example.com/icon.png"), ), ) - val json = McpJson.encodeToString(implementation) - - json shouldEqualJson """ + verifySerialization( + implementation, + McpJson, + """ { "name": "test-server", "version": "1.0.0", @@ -48,7 +54,8 @@ class CapabilitiesTest { {"src": "https://example.com/icon.png"} ] } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -62,7 +69,7 @@ class CapabilitiesTest { } """.trimIndent() - val implementation = McpJson.decodeFromString(json) + val implementation = verifyDeserialization(McpJson, json) assertEquals("test-server", implementation.name) assertEquals("2.1.3-beta", implementation.version) @@ -79,7 +86,7 @@ class CapabilitiesTest { } """.trimIndent() - val implementation = McpJson.decodeFromString(json) + val implementation = verifyDeserialization(McpJson, json) assertEquals("minimal-server", implementation.name) assertEquals("0.1.0", implementation.version) @@ -97,13 +104,7 @@ class CapabilitiesTest { websiteUrl = "https://test.com", ) - val json = McpJson.encodeToString(original) - val decoded = McpJson.decodeFromString(json) - - assertEquals(original.name, decoded.name) - assertEquals(original.version, decoded.version) - assertEquals(original.title, decoded.title) - assertEquals(original.websiteUrl, decoded.websiteUrl) + verifySerializationRoundTrip(original, McpJson) } // ClientCapabilities tests @@ -120,13 +121,15 @@ class CapabilitiesTest { val capabilities = ClientCapabilities( sampling = ClientCapabilities.sampling, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "sampling": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -134,13 +137,15 @@ class CapabilitiesTest { val capabilities = ClientCapabilities( roots = ClientCapabilities.Roots(), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "roots": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -148,15 +153,17 @@ class CapabilitiesTest { val capabilities = ClientCapabilities( roots = ClientCapabilities.Roots(listChanged = true), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "roots": { "listChanged": true } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -164,13 +171,15 @@ class CapabilitiesTest { val capabilities = ClientCapabilities( elicitation = ClientCapabilities.elicitation, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "elicitation": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -186,9 +195,10 @@ class CapabilitiesTest { val capabilities = ClientCapabilities( experimental = experimental, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "experimental": { "customFeature": { @@ -196,7 +206,8 @@ class CapabilitiesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -210,9 +221,10 @@ class CapabilitiesTest { elicitation = ClientCapabilities.elicitation, experimental = experimental, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "sampling": {}, "roots": { @@ -225,7 +237,8 @@ class CapabilitiesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -240,7 +253,7 @@ class CapabilitiesTest { } """.trimIndent() - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) assertEquals(EmptyJsonObject, capabilities.sampling) assertEquals(true, capabilities.roots?.listChanged) @@ -251,7 +264,7 @@ class CapabilitiesTest { fun `should deserialize empty ClientCapabilities from JSON`() { val json = "{}" - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) assertNull(capabilities.sampling) assertNull(capabilities.roots) @@ -267,12 +280,7 @@ class CapabilitiesTest { elicitation = ClientCapabilities.elicitation, ) - val json = McpJson.encodeToString(original) - val decoded = McpJson.decodeFromString(json) - - assertEquals(original.sampling, decoded.sampling) - assertEquals(original.roots?.listChanged, decoded.roots?.listChanged) - assertEquals(original.elicitation, decoded.elicitation) + verifySerializationRoundTrip(original, McpJson) } // ServerCapabilities tests @@ -289,13 +297,15 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( tools = ServerCapabilities.Tools(), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "tools": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -303,15 +313,17 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( tools = ServerCapabilities.Tools(listChanged = true), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "tools": { "listChanged": true } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -319,13 +331,15 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( resources = ServerCapabilities.Resources(), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "resources": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -333,15 +347,17 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( resources = ServerCapabilities.Resources(listChanged = true), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "resources": { "listChanged": true } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -349,15 +365,17 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( resources = ServerCapabilities.Resources(subscribe = true), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "resources": { "subscribe": true } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -368,16 +386,18 @@ class CapabilitiesTest { subscribe = false, ), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "resources": { "listChanged": true, "subscribe": false } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -385,13 +405,15 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( prompts = ServerCapabilities.Prompts(), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "prompts": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -399,15 +421,17 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( prompts = ServerCapabilities.Prompts(listChanged = false), ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "prompts": { "listChanged": false } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -415,13 +439,15 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( logging = ServerCapabilities.Logging, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "logging": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -429,13 +455,15 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( completions = ServerCapabilities.Completions, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "completions": {} } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -451,9 +479,10 @@ class CapabilitiesTest { val capabilities = ServerCapabilities( experimental = experimental, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "experimental": { "customCapability": { @@ -461,7 +490,8 @@ class CapabilitiesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -480,9 +510,10 @@ class CapabilitiesTest { completions = ServerCapabilities.Completions, experimental = experimental, ) - val json = McpJson.encodeToString(capabilities) - - json shouldEqualJson """ + verifySerialization( + capabilities, + McpJson, + """ { "tools": { "listChanged": true @@ -502,7 +533,8 @@ class CapabilitiesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -524,7 +556,7 @@ class CapabilitiesTest { } """.trimIndent() - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) assertEquals(true, capabilities.tools?.listChanged) assertEquals(false, capabilities.resources?.listChanged) @@ -538,7 +570,7 @@ class CapabilitiesTest { fun `should deserialize empty ServerCapabilities from JSON`() { val json = "{}" - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) assertNull(capabilities.tools) assertNull(capabilities.resources) @@ -560,14 +592,7 @@ class CapabilitiesTest { logging = ServerCapabilities.Logging, ) - val json = McpJson.encodeToString(original) - val decoded = McpJson.decodeFromString(json) - - assertEquals(original.tools?.listChanged, decoded.tools?.listChanged) - assertEquals(original.resources?.listChanged, decoded.resources?.listChanged) - assertEquals(original.resources?.subscribe, decoded.resources?.subscribe) - assertEquals(original.prompts?.listChanged, decoded.prompts?.listChanged) - assertEquals(original.logging, decoded.logging) + verifySerializationRoundTrip(original, McpJson) } // Additional nested type tests @@ -579,7 +604,7 @@ class CapabilitiesTest { } """.trimIndent() - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) assertNull(capabilities.roots?.listChanged) } @@ -594,7 +619,7 @@ class CapabilitiesTest { } """.trimIndent() - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) assertNull(capabilities.tools?.listChanged) assertNull(capabilities.resources?.listChanged) @@ -612,7 +637,7 @@ class CapabilitiesTest { } """.trimIndent() - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) // Should not fail - additionalProperties are allowed assertEquals("customValue", capabilities.sampling?.get("customProperty")?.toString()?.trim('"')) @@ -628,7 +653,7 @@ class CapabilitiesTest { } """.trimIndent() - val capabilities = McpJson.decodeFromString(json) + val capabilities = verifyDeserialization(McpJson, json) // Should not fail - additionalProperties are allowed assertEquals("debug", capabilities.logging?.get("level")?.toString()?.trim('"')) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt index 679d14a..44e88ac 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CommonTypeTest.kt @@ -1,10 +1,12 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerializationRoundTrip import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotEquals import kotlin.test.assertNull import kotlin.test.assertTrue @@ -12,29 +14,30 @@ class CommonTypeTest { @Test fun `should have correct latest protocol version`() { - assertNotEquals("", LATEST_PROTOCOL_VERSION) - assertEquals("2025-06-18", LATEST_PROTOCOL_VERSION) + LATEST_PROTOCOL_VERSION shouldBe "2025-06-18" } @Test fun `should have correct supported protocol versions`() { - assertIs>(SUPPORTED_PROTOCOL_VERSIONS) - assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains(LATEST_PROTOCOL_VERSION)) - assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains("2025-03-26")) - assertTrue(SUPPORTED_PROTOCOL_VERSIONS.contains("2024-11-05")) - assertEquals(3, SUPPORTED_PROTOCOL_VERSIONS.size) + SUPPORTED_PROTOCOL_VERSIONS shouldContainExactlyInAnyOrder listOf( + LATEST_PROTOCOL_VERSION, + "2025-03-26", + "2024-11-05", + ) } @Test fun `should serialize Icon with minimal fields`() { val icon = Icon(src = "https://example.com/icon.png") - val json = McpJson.encodeToString(icon) - - json shouldEqualJson """ + verifySerialization( + icon, + McpJson, + """ { "src": "https://example.com/icon.png" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -45,16 +48,18 @@ class CommonTypeTest { sizes = listOf("48x48", "96x96"), theme = Icon.Theme.Light, ) - val json = McpJson.encodeToString(icon) - - json shouldEqualJson """ + verifySerialization( + icon, + McpJson, + """ { "src": "https://example.com/icon.png", "mimeType": "image/png", "sizes": ["48x48", "96x96"], "theme": "light" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -68,7 +73,7 @@ class CommonTypeTest { } """.trimIndent() - val icon = McpJson.decodeFromString(json) + val icon = verifyDeserialization(McpJson, json) assertEquals("https://example.com/icon.png", icon.src) assertEquals("image/png", icon.mimeType) @@ -80,7 +85,7 @@ class CommonTypeTest { fun `should deserialize Icon with minimal fields from JSON`() { val json = """{"src": "https://example.com/icon.png"}""" - val icon = McpJson.decodeFromString(json) + val icon = verifyDeserialization(McpJson, json) assertEquals("https://example.com/icon.png", icon.src) assertNull(icon.mimeType) @@ -111,8 +116,8 @@ class CommonTypeTest { @Test fun `should deserialize Role from lowercase strings`() { - val user = McpJson.decodeFromString("\"user\"") - val assistant = McpJson.decodeFromString("\"assistant\"") + val user = verifyDeserialization(McpJson, "\"user\"") + val assistant = verifyDeserialization(McpJson, "\"assistant\"") assertEquals(Role.User, user) assertEquals(Role.Assistant, assistant) @@ -126,28 +131,32 @@ class CommonTypeTest { lastModified = "2025-01-12T15:00:58Z", ) - val json = McpJson.encodeToString(annotations) - - json shouldEqualJson """ + verifySerialization( + annotations, + McpJson, + """ { "audience": ["user", "assistant"], "priority": 0.8, "lastModified": "2025-01-12T15:00:58Z" } - """.trimIndent() + """.trimIndent(), + ) } @Test fun `should serialize Annotations with minimal fields`() { val annotations = Annotations(priority = 0.5) - val json = McpJson.encodeToString(annotations) - - json shouldEqualJson """ + verifySerialization( + annotations, + McpJson, + """ { "priority": 0.5 } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -160,7 +169,7 @@ class CommonTypeTest { } """.trimIndent() - val annotations = McpJson.decodeFromString(json) + val annotations = verifyDeserialization(McpJson, json) assertEquals(listOf(Role.User), annotations.audience) assertEquals(0.9, annotations.priority) @@ -175,11 +184,6 @@ class CommonTypeTest { lastModified = "2025-01-12T15:00:58Z", ) - val json = McpJson.encodeToString(original) - val decoded = McpJson.decodeFromString(json) - - assertEquals(original.audience, decoded.audience) - assertEquals(original.priority, decoded.priority) - assertEquals(original.lastModified, decoded.lastModified) + verifySerializationRoundTrip(original, McpJson) } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CompletionTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CompletionTest.kt index 9a27ac5..03a76e4 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CompletionTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/CompletionTest.kt @@ -1,6 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put @@ -25,9 +27,10 @@ class CompletionTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "completion/complete", "params": { @@ -41,7 +44,8 @@ class CompletionTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -67,9 +71,10 @@ class CompletionTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "completion/complete", "params": { @@ -92,7 +97,8 @@ class CompletionTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -121,7 +127,7 @@ class CompletionTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.CompletionComplete, request.method) val params = request.params @@ -148,9 +154,10 @@ class CompletionTest { }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "completion": { "values": [ @@ -164,7 +171,8 @@ class CompletionTest { "source": "cache" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -182,7 +190,7 @@ class CompletionTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals(listOf("README.md", "CONTRIBUTING.md"), result.completion.values) assertEquals(2, result.completion.total) assertEquals(false, result.completion.hasMore) @@ -209,7 +217,7 @@ class CompletionTest { } """.trimIndent() - val decoded = McpJson.decodeFromString(json) + val decoded = verifyDeserialization(McpJson, json) assertEquals(listOf("foo"), decoded.completion.values) assertNull(decoded.completion.total) assertNull(decoded.completion.hasMore) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt index 2290dce..f110326 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ContentTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put @@ -15,14 +16,16 @@ class ContentTest { fun `should serialize TextContent with minimal fields`() { val content = TextContent(text = "Hello, MCP!") - val json = McpJson.encodeToString(content) - - json shouldEqualJson """ + verifySerialization( + content, + McpJson, + """ { "type": "text", "text": "Hello, MCP!" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -33,9 +36,10 @@ class ContentTest { meta = buildJsonObject { put("origin", "assistant") }, ) - val json = McpJson.encodeToString(content) - - json shouldEqualJson """ + verifySerialization( + content, + McpJson, + """ { "type": "text", "text": "Need help?", @@ -46,12 +50,8 @@ class ContentTest { "origin": "assistant" } } - """.trimIndent() - - val decoded = McpJson.decodeFromString(json) as TextContent - assertEquals("Need help?", decoded.text) - assertEquals(listOf(Role.User), decoded.annotations?.audience) - assertEquals("assistant", decoded.meta?.get("origin")?.jsonPrimitive?.content) + """.trimIndent(), + ) } @Test @@ -61,15 +61,17 @@ class ContentTest { mimeType = "image/png", ) - val json = McpJson.encodeToString(content) - - json shouldEqualJson """ + verifySerialization( + content, + McpJson, + """ { "type": "image", "data": "aW1hZ2VEYXRh", "mimeType": "image/png" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -83,7 +85,7 @@ class ContentTest { } """.trimIndent() - val content = McpJson.decodeFromString(json) as ImageContent + val content = verifyDeserialization(McpJson, json) as ImageContent assertEquals("Zm9vYmFy", content.data) assertEquals("image/jpeg", content.mimeType) assertEquals(0.6, content.annotations?.priority) @@ -98,9 +100,10 @@ class ContentTest { meta = buildJsonObject { put("durationMs", 1200) }, ) - val json = McpJson.encodeToString(content) - - json shouldEqualJson """ + verifySerialization( + content, + McpJson, + """ { "type": "audio", "data": "YXVkaW9kYXRh", @@ -109,7 +112,8 @@ class ContentTest { "durationMs": 1200 } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -123,7 +127,7 @@ class ContentTest { } """.trimIndent() - val content = McpJson.decodeFromString(json) as AudioContent + val content = verifyDeserialization(McpJson, json) as AudioContent assertEquals("audio/mpeg", content.mimeType) assertEquals("YmF6", content.data) assertEquals("user", content.meta?.get("speaker")?.jsonPrimitive?.content) @@ -136,15 +140,17 @@ class ContentTest { uri = "file:///workspace/README.md", ) - val json = McpJson.encodeToString(resource) - - json shouldEqualJson """ + verifySerialization( + resource, + McpJson, + """ { "type": "resource_link", "name": "README", "uri": "file:///workspace/README.md" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -161,9 +167,10 @@ class ContentTest { meta = buildJsonObject { put("etag", "1234") }, ) - val json = McpJson.encodeToString(resource) - - json shouldEqualJson """ + verifySerialization( + resource, + McpJson, + """ { "type": "resource_link", "name": "README", @@ -184,7 +191,8 @@ class ContentTest { "etag": "1234" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -199,7 +207,7 @@ class ContentTest { } """.trimIndent() - val resource = McpJson.decodeFromString(json) as ResourceLink + val resource = verifyDeserialization(McpJson, json) as ResourceLink assertEquals("config", resource.name) assertEquals("file:///config.yaml", resource.uri) assertEquals(512, resource.size) @@ -220,9 +228,10 @@ class ContentTest { meta = buildJsonObject { put("source", "analysis") }, ) - val json = McpJson.encodeToString(embedded) - - json shouldEqualJson """ + verifySerialization( + embedded, + McpJson, + """ { "type": "resource", "resource": { @@ -240,7 +249,8 @@ class ContentTest { "source": "analysis" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -257,7 +267,7 @@ class ContentTest { } """.trimIndent() - val embedded = McpJson.decodeFromString(json) as EmbeddedResource + val embedded = verifyDeserialization(McpJson, json) as EmbeddedResource assertEquals(ContentTypes.EMBEDDED_RESOURCE, embedded.type) assertNull(embedded.annotations) assertEquals("base64", embedded.meta?.get("encoding")?.jsonPrimitive?.content) @@ -292,7 +302,7 @@ class ContentTest { ] """.trimIndent() - val content = McpJson.decodeFromString>(json) + val content = verifyDeserialization>(McpJson, json) assertEquals(3, content.size) assertIs(content[0]) assertIs(content[1]) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt index c59c23f..0026ea0 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ElicitationTest.kt @@ -1,6 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int @@ -16,6 +18,7 @@ import kotlin.test.assertNull class ElicitationTest { @Test + @Suppress("LongMethod") fun `should serialize ElicitRequest with requested schema`() { val request = ElicitRequest( ElicitRequestParams( @@ -54,9 +57,10 @@ class ElicitationTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "elicitation/create", "params": { @@ -84,7 +88,8 @@ class ElicitationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -111,7 +116,7 @@ class ElicitationTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.ElicitationCreate, request.method) val params = request.params @@ -163,7 +168,7 @@ class ElicitationTest { } """.trimIndent() - val decoded = McpJson.decodeFromString(json) + val decoded = verifyDeserialization(McpJson, json) assertEquals(ElicitResult.Action.Accept, decoded.action) val content = decoded.content @@ -188,7 +193,7 @@ class ElicitationTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals(ElicitResult.Action.Decline, result.action) assertNull(result.content) assertEquals("User skipped", result.meta?.get("reason")?.jsonPrimitive?.content) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt index 6af7c4e..69c0878 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/InitializeTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonObject @@ -14,6 +15,7 @@ import kotlin.test.assertNull class InitializeTest { @Test + @Suppress("LongMethod") fun `should serialize InitializeRequest with capabilities and meta`() { val request = InitializeRequest( InitializeRequestParams( @@ -41,9 +43,10 @@ class InitializeTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "initialize", "params": { @@ -73,7 +76,8 @@ class InitializeTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -106,7 +110,7 @@ class InitializeTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.Initialize, request.method) val params = request.params @@ -149,9 +153,10 @@ class InitializeTest { meta = buildJsonObject { put("issuedAt", "2025-01-12T15:00:58Z") }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "protocolVersion": "2024-11-05", "capabilities": { @@ -180,7 +185,8 @@ class InitializeTest { "issuedAt": "2025-01-12T15:00:58Z" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -201,7 +207,7 @@ class InitializeTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals("2025-03-26", result.protocolVersion) assertEquals("result-server", result.serverInfo.name) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt index ce02cc0..db111fe 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/JsonRpcTest.kt @@ -3,6 +3,8 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeSameInstanceAs +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int @@ -43,7 +45,8 @@ class JsonRpcTest { @Test fun `should convert JSONRPCRequest to Request`() { - val jsonRpc = McpJson.decodeFromString( + val jsonRpc = verifyDeserialization( + McpJson, """ { "id": 17, @@ -101,7 +104,8 @@ class JsonRpcTest { @Test fun `should convert JSONRPCNotification to Notification`() { - val jsonRpc = McpJson.decodeFromString( + val jsonRpc = verifyDeserialization( + McpJson, """ { "method": "notifications/message", @@ -147,9 +151,10 @@ class JsonRpcTest { }, ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "id": "req-1", "method": "tools/list", @@ -159,7 +164,8 @@ class JsonRpcTest { }, "jsonrpc": "2.0" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -175,7 +181,7 @@ class JsonRpcTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) val id = request.id assertIs(id) @@ -196,9 +202,10 @@ class JsonRpcTest { }, ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/log", "params": { @@ -207,7 +214,8 @@ class JsonRpcTest { }, "jsonrpc": "2.0" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -223,7 +231,7 @@ class JsonRpcTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) assertEquals("notifications/progress", notification.method) val params = notification.params?.jsonObject assertNotNull(params) @@ -240,9 +248,10 @@ class JsonRpcTest { ), ) - val json = McpJson.encodeToString(response) - - json shouldEqualJson """ + verifySerialization( + response, + McpJson, + """ { "id": "call-1", "result": { @@ -252,7 +261,8 @@ class JsonRpcTest { }, "jsonrpc": "2.0" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -269,7 +279,7 @@ class JsonRpcTest { } """.trimIndent() - val response = McpJson.decodeFromString(json) + val response = verifyDeserialization(McpJson, json) val result = response.result assertIs(result) assertEquals(RequestId.NumberId(7L), response.id) @@ -289,9 +299,10 @@ class JsonRpcTest { ), ) - val json = McpJson.encodeToString(error) - - json shouldEqualJson """ + verifySerialization( + error, + McpJson, + """ { "id": 99, "error": { @@ -303,7 +314,8 @@ class JsonRpcTest { }, "jsonrpc": "2.0" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -323,7 +335,7 @@ class JsonRpcTest { } """.trimIndent() - val error = McpJson.decodeFromString(json) + val error = verifyDeserialization(McpJson, json) assertEquals(RequestId("req-404"), error.id) assertEquals(RPCError.ErrorCode.INVALID_PARAMS, error.error.code) assertEquals("Invalid params", error.error.message) @@ -348,7 +360,7 @@ class JsonRpcTest { } """.trimIndent() - val message = McpJson.decodeFromString(json) + val message = verifyDeserialization(McpJson, json) val request = assertIs(message) assertEquals("sampling/create", request.method) val params = request.params?.jsonObject @@ -373,7 +385,7 @@ class JsonRpcTest { } """.trimIndent() - val message = McpJson.decodeFromString(json) + val message = verifyDeserialization(McpJson, json) val error = assertIs(message) assertEquals(RPCError.ErrorCode.REQUEST_TIMEOUT, error.error.code) val data = error.error.data?.jsonObject @@ -388,9 +400,10 @@ class JsonRpcTest { params = buildJsonObject { put("message", "Polymorphic") }, ) - val json = McpJson.encodeToString(message) - - json shouldEqualJson """ + verifySerialization( + message, + McpJson, + """ { "method": "notifications/log", "params": { @@ -398,7 +411,8 @@ class JsonRpcTest { }, "jsonrpc": "2.0" } - """.trimIndent() + """.trimIndent(), + ) } @Test diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/LoggingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/LoggingTest.kt index 2b8c61a..bc03a47 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/LoggingTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/LoggingTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonObject @@ -47,9 +48,10 @@ class LoggingTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "logging/setLevel", "params": { @@ -59,7 +61,8 @@ class LoggingTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -76,7 +79,7 @@ class LoggingTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.LoggingSetLevel, request.method) assertEquals(LoggingLevel.Alert, request.params.level) assertEquals(ProgressToken(1001), request.params.meta?.progressToken) @@ -93,9 +96,10 @@ class LoggingTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/message", "params": { @@ -109,7 +113,8 @@ class LoggingTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -124,7 +129,7 @@ class LoggingTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.NotificationsMessage, notification.method) val params = notification.params @@ -153,7 +158,7 @@ class LoggingTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) val params = notification.params assertEquals(LoggingLevel.Critical, params.level) val data = params.data.jsonObject diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt index bbd209d..b55a441 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/NotificationTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.put @@ -20,9 +21,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/cancelled", "params": { @@ -33,7 +35,8 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -48,7 +51,7 @@ class NotificationTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) val params = notification.params assertEquals(Method.Defined.NotificationsCancelled, notification.method) @@ -65,9 +68,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/initialized", "params": { @@ -76,7 +80,8 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -87,7 +92,7 @@ class NotificationTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.NotificationsInitialized, notification.method) assertNull(notification.params) @@ -105,9 +110,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/progress", "params": { @@ -120,7 +126,8 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -135,7 +142,7 @@ class NotificationTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) val params = notification.params assertEquals(Method.Defined.NotificationsProgress, notification.method) @@ -155,9 +162,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/resources/updated", "params": { @@ -167,7 +175,8 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -184,7 +193,7 @@ class NotificationTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) val params = notification.params assertEquals(Method.Defined.NotificationsResourcesUpdated, notification.method) @@ -200,9 +209,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/prompts/list_changed", "params": { @@ -211,7 +221,8 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -222,7 +233,7 @@ class NotificationTest { } """.trimIndent() - val notification = McpJson.decodeFromString(json) + val notification = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.NotificationsPromptsListChanged, notification.method) assertNull(notification.params) @@ -236,9 +247,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/resources/list_changed", "params": { @@ -247,7 +259,8 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -258,9 +271,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/roots/list_changed", "params": { @@ -269,7 +283,8 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -280,9 +295,10 @@ class NotificationTest { ), ) - val json = McpJson.encodeToString(notification) - - json shouldEqualJson """ + verifySerialization( + notification, + McpJson, + """ { "method": "notifications/tools/list_changed", "params": { @@ -291,6 +307,7 @@ class NotificationTest { } } } - """.trimIndent() + """.trimIndent(), + ) } } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt index 070d58e..72bddb0 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PingRequestTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject @@ -17,13 +18,15 @@ class PingRequestTest { fun `should serialize PingRequest without params`() { val request = PingRequest() - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "ping" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -36,9 +39,10 @@ class PingRequestTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "ping", "params": { @@ -47,7 +51,8 @@ class PingRequestTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -84,7 +89,7 @@ class PingRequestTest { } """.trimIndent() - val jsonRpc = McpJson.decodeFromString(json) + val jsonRpc = verifyDeserialization(McpJson, json) val request = jsonRpc.fromJSON() val pingRequest = assertIs(request) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PromptsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PromptsTest.kt index 01eb01c..d7101d3 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PromptsTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/PromptsTest.kt @@ -1,6 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.double import kotlinx.serialization.json.int @@ -18,13 +20,15 @@ class PromptsTest { fun `should serialize PromptArgument with minimal fields`() { val argument = PromptArgument(name = "repository") - val json = McpJson.encodeToString(argument) - - json shouldEqualJson """ + verifySerialization( + argument, + McpJson, + """ { "name": "repository" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -36,16 +40,18 @@ class PromptsTest { title = "Language", ) - val json = McpJson.encodeToString(argument) - - json shouldEqualJson """ + verifySerialization( + argument, + McpJson, + """ { "name": "language", "description": "Programming language to use", "required": true, "title": "Language" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -69,9 +75,10 @@ class PromptsTest { meta = buildJsonObject { put("category", "status-report") }, ) - val json = McpJson.encodeToString(prompt) - - json shouldEqualJson """ + verifySerialization( + prompt, + McpJson, + """ { "name": "summarize_update", "description": "Summarize the latest repository changes.", @@ -97,7 +104,8 @@ class PromptsTest { "category": "status-report" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -127,7 +135,7 @@ class PromptsTest { } """.trimIndent() - val prompt = McpJson.decodeFromString(json) + val prompt = verifyDeserialization(McpJson, json) assertEquals("code_review", prompt.name) assertEquals("Generate a succinct code review.", prompt.description) @@ -151,9 +159,10 @@ class PromptsTest { content = TextContent(text = "Provide a concise summary of the changes."), ) - val json = McpJson.encodeToString(message) - - json shouldEqualJson """ + verifySerialization( + message, + McpJson, + """ { "role": "assistant", "content": { @@ -161,7 +170,8 @@ class PromptsTest { "text": "Provide a concise summary of the changes." } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -180,7 +190,7 @@ class PromptsTest { } """.trimIndent() - val message = McpJson.decodeFromString(json) + val message = verifyDeserialization(McpJson, json) assertEquals(Role.User, message.role) val content = message.content @@ -203,7 +213,7 @@ class PromptsTest { } """.trimIndent() - val decoded = McpJson.decodeFromString(json) + val decoded = verifyDeserialization(McpJson, json) val promptReference = assertIs(decoded) assertEquals("daily-summary", promptReference.name) assertEquals("Daily Summary", promptReference.title) @@ -222,9 +232,10 @@ class PromptsTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "prompts/get", "params": { @@ -237,7 +248,8 @@ class PromptsTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -258,7 +270,7 @@ class PromptsTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.PromptsGet, request.method) val params = request.params @@ -287,9 +299,10 @@ class PromptsTest { meta = buildJsonObject { put("generatedAt", "2025-01-12T15:00:58Z") }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "messages": [ { @@ -312,7 +325,8 @@ class PromptsTest { "generatedAt": "2025-01-12T15:00:58Z" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -332,7 +346,7 @@ class PromptsTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) val messages = result.messages assertEquals(1, messages.size) @@ -354,9 +368,10 @@ class PromptsTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "prompts/list", "params": { @@ -366,20 +381,23 @@ class PromptsTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test fun `should serialize ListPromptsRequest without params`() { val request = ListPromptsRequest() - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "prompts/list" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -393,9 +411,10 @@ class PromptsTest { meta = buildJsonObject { put("page", 1) }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "prompts": [ {"name": "morning-briefing"}, @@ -406,7 +425,8 @@ class PromptsTest { "page": 1 } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -433,7 +453,7 @@ class PromptsTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) val prompts = result.prompts assertEquals(1, prompts.size) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt index c42589e..1d6da19 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RequestTest.kt @@ -2,6 +2,8 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson import io.kotest.matchers.shouldBe +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.double import kotlinx.serialization.json.jsonPrimitive @@ -41,16 +43,18 @@ class RequestTest { ), ) - val json = McpJson.encodeToString(params) - - json shouldEqualJson """ + verifySerialization( + params, + McpJson, + """ { "_meta": { "progressToken": "base-42", "origin": "test-suite" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -64,7 +68,7 @@ class RequestTest { } """.trimIndent() - val params = McpJson.decodeFromString(json) + val params = verifyDeserialization(McpJson, json) val meta = params.meta assertNotNull(meta) @@ -79,16 +83,18 @@ class RequestTest { meta = RequestMeta(buildJsonObject { put("progressToken", "page-req") }), ) - val json = McpJson.encodeToString(params) - - json shouldEqualJson """ + verifySerialization( + params, + McpJson, + """ { "cursor": "cursor-1", "_meta": { "progressToken": "page-req" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -102,7 +108,7 @@ class RequestTest { } """.trimIndent() - val params = McpJson.decodeFromString(json) + val params = verifyDeserialization(McpJson, json) assertEquals("cursor-2", params.cursor) assertEquals(ProgressToken(99), params.meta?.progressToken) @@ -163,22 +169,24 @@ class RequestTest { }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "_meta": { "processedBy": "worker-1" } } - """.trimIndent() + """.trimIndent(), + ) } @Test fun `should deserialize EmptyResult without meta`() { val json = "{}" - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertNull(result.meta) } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ResourcesTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ResourcesTest.kt index 0313b75..e47a0f5 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ResourcesTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ResourcesTest.kt @@ -1,6 +1,8 @@ package io.modelcontextprotocol.kotlin.sdk.types import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonPrimitive @@ -20,14 +22,16 @@ class ResourcesTest { name = "README", ) - val json = McpJson.encodeToString(resource) - - json shouldEqualJson """ + verifySerialization( + resource, + McpJson, + """ { "uri": "file:///workspace/README.md", "name": "README" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -46,9 +50,10 @@ class ResourcesTest { meta = buildJsonObject { put("etag", "abc123") }, ) - val json = McpJson.encodeToString(resource) - - json shouldEqualJson """ + verifySerialization( + resource, + McpJson, + """ { "uri": "file:///workspace/CHANGELOG.md", "name": "CHANGELOG", @@ -67,7 +72,8 @@ class ResourcesTest { "etag": "abc123" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -92,7 +98,7 @@ class ResourcesTest { } """.trimIndent() - val resource = McpJson.decodeFromString(json) + val resource = verifyDeserialization(McpJson, json) assertEquals("file:///config/settings.json", resource.uri) assertEquals("config-settings", resource.name) @@ -122,9 +128,10 @@ class ResourcesTest { meta = buildJsonObject { put("requiresAuth", true) }, ) - val json = McpJson.encodeToString(template) - - json shouldEqualJson """ + verifySerialization( + template, + McpJson, + """ { "uriTemplate": "file:///workspace/{path}", "name": "workspace-file", @@ -145,7 +152,8 @@ class ResourcesTest { "requiresAuth": true } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -161,7 +169,7 @@ class ResourcesTest { } """.trimIndent() - val template = McpJson.decodeFromString(json) + val template = verifyDeserialization(McpJson, json) assertEquals("db://users/{id}", template.uriTemplate) assertEquals("user-record", template.name) @@ -183,7 +191,7 @@ class ResourcesTest { } """.trimIndent() - val decoded = McpJson.decodeFromString(json) + val decoded = verifyDeserialization(McpJson, json) val templateReference = assertIs(decoded) assertEquals("file:///workspace/{path}", templateReference.uri) assertEquals(ReferenceType.ResourceTemplate, templateReference.type) @@ -198,9 +206,10 @@ class ResourcesTest { meta = buildJsonObject { put("encoding", "utf-8") }, ) - val json = McpJson.encodeToString(contents) - - json shouldEqualJson """ + verifySerialization( + contents, + McpJson, + """ { "text": "Hello, MCP!", "uri": "file:///workspace/hello.txt", @@ -209,7 +218,8 @@ class ResourcesTest { "encoding": "utf-8" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -225,7 +235,7 @@ class ResourcesTest { } """.trimIndent() - val contents = McpJson.decodeFromString(json) + val contents = verifyDeserialization(McpJson, json) val blobContents = assertIs(contents) assertEquals("YmluYXJ5ZGF0YQ==", blobContents.blob) assertEquals("file:///workspace/logo.png", blobContents.uri) @@ -240,14 +250,16 @@ class ResourcesTest { mimeType = "application/octet-stream", ) - val json = McpJson.encodeToString(contents) - - json shouldEqualJson """ + verifySerialization( + contents, + McpJson, + """ { "uri": "custom://resource/42", "mimeType": "application/octet-stream" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -259,9 +271,10 @@ class ResourcesTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "resources/list", "params": { @@ -271,20 +284,23 @@ class ResourcesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test fun `should serialize ListResourcesRequest without params`() { val request = ListResourcesRequest() - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "resources/list" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -298,9 +314,10 @@ class ResourcesTest { meta = buildJsonObject { put("page", 1) }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "resources": [ {"uri": "file:///workspace/README.md", "name": "README"}, @@ -311,7 +328,8 @@ class ResourcesTest { "page": 1 } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -332,7 +350,7 @@ class ResourcesTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals("cursor-next", result.nextCursor) val resources = result.resources @@ -353,9 +371,10 @@ class ResourcesTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "resources/read", "params": { @@ -365,7 +384,8 @@ class ResourcesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -390,7 +410,7 @@ class ResourcesTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) val contents = result.contents assertEquals(2, contents.size) @@ -410,9 +430,10 @@ class ResourcesTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "resources/subscribe", "params": { @@ -422,7 +443,8 @@ class ResourcesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -434,9 +456,10 @@ class ResourcesTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "resources/unsubscribe", "params": { @@ -446,7 +469,8 @@ class ResourcesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -458,9 +482,10 @@ class ResourcesTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "resources/templates/list", "params": { @@ -470,7 +495,8 @@ class ResourcesTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -483,9 +509,10 @@ class ResourcesTest { meta = buildJsonObject { put("page", 3) }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "resourceTemplates": [ { @@ -498,7 +525,8 @@ class ResourcesTest { "page": 3 } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -522,7 +550,7 @@ class ResourcesTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals("cursor-next", result.nextCursor) val templates = result.resourceTemplates diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RootsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RootsTest.kt index 3f1b8d6..2350c25 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RootsTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/RootsTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.boolean import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonPrimitive @@ -15,13 +16,15 @@ class RootsTest { fun `should serialize Root with minimal fields`() { val root = Root(uri = "file:///workspace/project") - val json = McpJson.encodeToString(root) - - json shouldEqualJson """ + verifySerialization( + root, + McpJson, + """ { "uri": "file:///workspace/project" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -32,9 +35,10 @@ class RootsTest { meta = buildJsonObject { put("writable", true) }, ) - val json = McpJson.encodeToString(root) - - json shouldEqualJson """ + verifySerialization( + root, + McpJson, + """ { "uri": "file:///workspace/docs", "name": "Docs", @@ -42,7 +46,8 @@ class RootsTest { "writable": true } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -57,7 +62,7 @@ class RootsTest { } """.trimIndent() - val root = McpJson.decodeFromString(json) + val root = verifyDeserialization(McpJson, json) assertEquals("file:///workspace/code", root.uri) assertEquals("Code", root.name) @@ -74,9 +79,10 @@ class RootsTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "roots/list", "params": { @@ -85,20 +91,23 @@ class RootsTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test fun `should serialize ListRootsRequest without params`() { val request = ListRootsRequest() - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "roots/list" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -111,9 +120,10 @@ class RootsTest { meta = buildJsonObject { put("issuedAt", "2025-01-12T15:00:58Z") }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "roots": [ {"uri": "file:///workspace/project", "name": "Project"}, @@ -123,7 +133,8 @@ class RootsTest { "issuedAt": "2025-01-12T15:00:58Z" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -142,7 +153,7 @@ class RootsTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) val roots = result.roots assertEquals(1, roots.size) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt index 7dc3fe6..696f322 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/SamplingTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.double import kotlinx.serialization.json.jsonPrimitive @@ -17,13 +18,15 @@ class SamplingTest { fun `should serialize ModelHint`() { val hint = ModelHint(name = "claude-3-5-sonnet") - val json = McpJson.encodeToString(hint) - - json shouldEqualJson """ + verifySerialization( + hint, + McpJson, + """ { "name": "claude-3-5-sonnet" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -35,9 +38,10 @@ class SamplingTest { intelligencePriority = 1.0, ) - val json = McpJson.encodeToString(preferences) - - json shouldEqualJson """ + verifySerialization( + preferences, + McpJson, + """ { "hints": [ {"name": "haiku"}, @@ -47,7 +51,8 @@ class SamplingTest { "speedPriority": 0.75, "intelligencePriority": 1.0 } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -64,9 +69,10 @@ class SamplingTest { content = TextContent(text = "Summarize the latest release."), ) - val json = McpJson.encodeToString(message) - - json shouldEqualJson """ + verifySerialization( + message, + McpJson, + """ { "role": "user", "content": { @@ -74,10 +80,12 @@ class SamplingTest { "text": "Summarize the latest release." } } - """.trimIndent() + """.trimIndent(), + ) } @Test + @Suppress("LongMethod") fun `should serialize CreateMessageRequest with all fields`() { val request = CreateMessageRequest( CreateMessageRequestParams( @@ -105,9 +113,10 @@ class SamplingTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "sampling/createMessage", "params": { @@ -146,7 +155,8 @@ class SamplingTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -181,7 +191,7 @@ class SamplingTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.SamplingCreateMessage, request.method) val params = request.params @@ -209,9 +219,10 @@ class SamplingTest { meta = buildJsonObject { put("latencyMs", 850) }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "role": "assistant", "content": { @@ -224,7 +235,8 @@ class SamplingTest { "latencyMs": 850 } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -244,7 +256,7 @@ class SamplingTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals(Role.Assistant, result.role) val text = assertIs(result.content) diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt index 447df4f..4663fbd 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/ToolsTest.kt @@ -1,6 +1,7 @@ package io.modelcontextprotocol.kotlin.sdk.types -import io.kotest.assertions.json.shouldEqualJson +import io.modelcontextprotocol.kotlin.test.utils.verifyDeserialization +import io.modelcontextprotocol.kotlin.test.utils.verifySerialization import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int @@ -21,19 +22,22 @@ class ToolsTest { inputSchema = ToolSchema(), ) - val json = McpJson.encodeToString(tool) - - json shouldEqualJson """ + verifySerialization( + tool, + McpJson, + """ { "name": "search", "inputSchema": { "type": "object" } } - """.trimIndent() + """.trimIndent(), + ) } @Test + @Suppress("LongMethod") fun `should serialize Tool with annotations and schemas`() { val inputSchema = ToolSchema( properties = buildJsonObject { @@ -74,9 +78,10 @@ class ToolsTest { meta = buildJsonObject { put("category", "search") }, ) - val json = McpJson.encodeToString(tool) - - json shouldEqualJson """ + verifySerialization( + tool, + McpJson, + """ { "name": "web-search", "inputSchema": { @@ -113,7 +118,8 @@ class ToolsTest { "category": "search" } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -139,7 +145,7 @@ class ToolsTest { } """.trimIndent() - val tool = McpJson.decodeFromString(json) + val tool = verifyDeserialization(McpJson, json) assertEquals("translate", tool.name) assertEquals("Translate Text", tool.annotations?.title) @@ -156,9 +162,7 @@ class ToolsTest { fun `should serialize ToolSchema with defs`() { val tool = toolWithDefs() - val json = McpJson.encodeToString(tool) - - json shouldEqualJson toolWithDefsJson() + verifySerialization(tool, McpJson, toolWithDefsJson()) } @Test @@ -188,7 +192,7 @@ class ToolsTest { } """.trimIndent() - val tool = McpJson.decodeFromString(json) + val tool = verifyDeserialization(McpJson, json) val schema = tool.inputSchema val defs = schema.defs @@ -215,9 +219,10 @@ class ToolsTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "tools/call", "params": { @@ -230,7 +235,8 @@ class ToolsTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -250,7 +256,7 @@ class ToolsTest { } """.trimIndent() - val request = McpJson.decodeFromString(json) + val request = verifyDeserialization(McpJson, json) assertEquals(Method.Defined.ToolsCall, request.method) val params = request.params @@ -273,9 +279,10 @@ class ToolsTest { meta = buildJsonObject { put("elapsedMs", 1200) }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "content": [ { @@ -294,7 +301,8 @@ class ToolsTest { "elapsedMs": 1200 } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -318,7 +326,7 @@ class ToolsTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals(true, result.isError) val text = assertIs(result.content.first()) @@ -338,9 +346,10 @@ class ToolsTest { ), ) - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "tools/list", "params": { @@ -350,20 +359,23 @@ class ToolsTest { } } } - """.trimIndent() + """.trimIndent(), + ) } @Test fun `should serialize ListToolsRequest without params`() { val request = ListToolsRequest() - val json = McpJson.encodeToString(request) - - json shouldEqualJson """ + verifySerialization( + request, + McpJson, + """ { "method": "tools/list" } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -377,9 +389,10 @@ class ToolsTest { meta = buildJsonObject { put("page", 1) }, ) - val json = McpJson.encodeToString(result) - - json shouldEqualJson """ + verifySerialization( + result, + McpJson, + """ { "tools": [ { @@ -400,7 +413,8 @@ class ToolsTest { "page": 1 } } - """.trimIndent() + """.trimIndent(), + ) } @Test @@ -423,7 +437,7 @@ class ToolsTest { } """.trimIndent() - val result = McpJson.decodeFromString(json) + val result = verifyDeserialization(McpJson, json) assertEquals("cursor-next", result.nextCursor) val tools = result.tools @@ -483,7 +497,7 @@ class ToolsTest { @Test fun `should deserialize complex tool definition`() { val expected = weatherTool() - val actual = McpJson.decodeFromString(weatherToolJson()) + val actual = verifyDeserialization(McpJson, weatherToolJson()) assertEquals(expected, actual) } diff --git a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt index 5b60cfb..c53a400 100644 --- a/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt +++ b/kotlin-sdk-core/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/types/dsl/SamplingDslTest.kt @@ -28,6 +28,7 @@ import kotlin.test.Test @OptIn(ExperimentalMcpApi::class) class SamplingDslTest { @Test + @Suppress("LongMethod") fun `buildCreateMessageRequest should build with all fields`() { val request = buildCreateMessageRequest { maxTokens = 1000 diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index 346001a..91574e0 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -1,24 +1,56 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + plugins { - kotlin("jvm") + id("mcp.multiplatform") } kotlin { - explicitApi() - dependencies { - api(kotlin("test")) - api(libs.kotest.assertions.core) - api(libs.kotest.assertions.json) - api(libs.kotlinx.coroutines.test) - api(libs.ktor.server.core) - api(libs.mockk) - api(libs.awaitility) - api(libs.junit.jupiter.params) - api(libs.kotlin.logging) - runtimeOnly(libs.slf4j.simple) + iosArm64() + iosX64() + iosSimulatorArm64() + watchosX64() + watchosArm64() + watchosSimulatorArm64() + tvosX64() + tvosArm64() + tvosSimulatorArm64() + js { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() } - tasks.withType { - useJUnitPlatform() + sourceSets { + commonMain { + dependencies { + + api(kotlin("test")) + api(libs.kotest.assertions.core) + api(libs.kotest.assertions.json) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.coroutines.test) + api(libs.ktor.server.core) + api(libs.kotlin.logging) + } + } + + jvmMain { + dependencies { + api(libs.mockk) + api(libs.awaitility) + api(libs.junit.jupiter.params) + runtimeOnly(libs.slf4j.simple) + } + } } } + +tasks.withType { + useJUnitPlatform() +} diff --git a/test-utils/detekt-baseline.xml b/test-utils/detekt-baseline.xml index 390ccd0..5abd0b4 100644 --- a/test-utils/detekt-baseline.xml +++ b/test-utils/detekt-baseline.xml @@ -3,7 +3,6 @@ CyclomaticComplexMethod:Retry.kt:RetryExtension$private fun executeWithRetry - ReturnCount:TypeScriptRunner.kt:TypeScriptRunner$public fun installDependencies ThrowsCount:Retry.kt:RetryExtension$private fun executeWithRetry diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/ktor.kt b/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/ktor.kt similarity index 100% rename from test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/ktor.kt rename to test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/ktor.kt diff --git a/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/serializationUtils.kt b/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/serializationUtils.kt new file mode 100644 index 0000000..8f3d33a --- /dev/null +++ b/test-utils/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/serializationUtils.kt @@ -0,0 +1,88 @@ +package io.modelcontextprotocol.kotlin.test.utils + +import io.kotest.assertions.json.shouldEqualJson +import io.kotest.assertions.withClue +import io.kotest.matchers.shouldBe +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.intellij.lang.annotations.Language +import kotlin.test.fail + +/** + * Verifies the serialization and deserialization process for a given object, ensuring: + * - The object can be serialized to JSON. + * - The serialized JSON matches the expected JSON. + * - The serialized JSON can be deserialized back into the original object. + * + * This is especially useful for round-trip serialization tests. + * + * @param T The type of the object to be verified. + * @param value The object to be serialized and deserialized. + * @param json The JSON serializer/deserializer used for the serialization process. + * @param expectedJson A lambda returning the expected JSON string for comparison purposes. + */ +public inline fun verifySerialization(value: T, json: Json, expectedJson: () -> String) { + verifySerialization(value, json, expectedJson.invoke()) +} + +/** + * Verifies the serialization and deserialization process for a given object, ensuring: + * - The object can be serialized to JSON. + * - The serialized JSON matches the expected JSON. + * - The serialized JSON can be deserialized back into the original object. + * + * This is especially useful for round-trip serialization tests. + * + * @param T The type of the object being tested for serialization and deserialization. + * @param value The object to be serialized and deserialized. + * @param json The JSON serialization/deserialization processor. + * @param expectedJson The expected JSON string to compare against the serialized output. + */ +public inline fun verifySerialization(value: T, json: Json, @Language("json") expectedJson: String) { + val jsonString = json.encodeToString(value) + + jsonString shouldEqualJson expectedJson + + val parsedObject: T = try { + json.decodeFromString(jsonString) + } catch (e: SerializationException) { + fail("Failed to parse generated json string: ${e.message}:\n$value", e) + } + withClue("Parsed object is not equal to original value") { + parsedObject shouldBe value + } +} + +/** + * Verifies the deserialization of a JSON payload into an object of the specified type, + * and ensures that the reverse serialization process returns the same JSON payload. + * + * This is especially useful for round-trip serialization tests. + * + * @param T The type of the object being deserialized from the JSON payload. + * @param json The JSON processor used for deserialization and serialization. + * @param payload The JSON string to deserialize into an object and verify against. + * @return The deserialized object of type T. + */ +public inline fun verifyDeserialization(json: Json, @Language("json") payload: String): T { + val value: T = json.decodeFromString(payload) + verifySerialization(value = value, json = json, expectedJson = payload) + return value +} + +/** + * Verifies the round-trip serialization and deserialization process for a given object. + * This method ensures that: + * - The object can be serialized into a JSON string. + * - The serialized JSON string can be deserialized back into an equivalent object. + * + * This is particularly useful for testing the integrity of serialization logic. + * + * @param T The type of the object being tested for serialization and deserialization. + * @param value The object to be serialized and deserialized. + * @param json The JSON processor used for the serialization and deserialization process. + */ +public inline fun verifySerializationRoundTrip(value: T, json: Json) { + val jsonString: String = json.encodeToString(value) + verifySerialization(value = value, json = json, expectedJson = jsonString) +} diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/DisabledOnCI.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/DisabledOnCI.kt similarity index 100% rename from test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/DisabledOnCI.kt rename to test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/DisabledOnCI.kt diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt similarity index 96% rename from test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt rename to test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt index e173f3f..a37e6e3 100644 --- a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/Retry.kt @@ -16,6 +16,7 @@ import java.util.Optional @ExtendWith(RetryExtension::class) public annotation class Retry(val times: Int = 3, val delayMs: Long = 1000) +@Suppress("ForbiddenVoid") // needed for Java API interop public class RetryExtension : InvocationInterceptor { override fun interceptTestMethod( invocation: Invocation, @@ -87,7 +88,7 @@ public class RetryExtension : InvocationInterceptor { } } - throw lastError ?: IllegalStateException("Unexpected state in retry logic") + throw lastError ?: error("Unexpected state in retry logic") } private fun describeTest(ctx: ExtensionContext): String { diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt similarity index 98% rename from test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt rename to test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt index ff9292e..e7f5798 100644 --- a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt +++ b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/TypeScriptRunner.kt @@ -18,6 +18,7 @@ public object TypeScriptRunner { * @param logPrefix Prefix for log lines * @return The started Process */ + @Suppress("LongParameterList") public fun run( typescriptDir: File, scriptPath: String, @@ -53,6 +54,7 @@ public object TypeScriptRunner { * @param typescriptDir The directory containing package.json * @throws RuntimeException if npm install fails */ + @Suppress("ReturnCount") public fun installDependencies(typescriptDir: File) { require(typescriptDir.isDirectory()) { "Type script directory does not exist: ${typescriptDir.absolutePath}" } println("Installing TypeScript dependencies in ${typescriptDir.absolutePath}") diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/platform.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/platform.jvm.kt similarity index 100% rename from test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/platform.kt rename to test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/platform.jvm.kt diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processBuilders.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processBuilders.kt similarity index 100% rename from test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processBuilders.kt rename to test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processBuilders.kt diff --git a/test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt b/test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt similarity index 100% rename from test-utils/src/main/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt rename to test-utils/src/jvmMain/kotlin/io/modelcontextprotocol/kotlin/test/utils/processUtils.kt