diff --git a/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorClientTestSuite.kt b/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorClientTestSuite.kt index 792a7ae1b..1ed9958f8 100644 --- a/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorClientTestSuite.kt +++ b/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorClientTestSuite.kt @@ -5,11 +5,13 @@ import io.ktor.client.engine.* import io.ktor.client.plugins.websocket.* import org.hildan.krossbow.websocket.* import org.hildan.krossbow.websocket.test.* +import kotlin.time.Duration abstract class KtorClientTestSuite( statusCodeSupport: StatusCodeSupport = StatusCodeSupport.All, shouldTestNegotiatedSubprotocol: Boolean = true, -) : WebSocketClientTestSuite(statusCodeSupport, shouldTestNegotiatedSubprotocol) { + headersTestDelay: Duration? = null, +) : WebSocketClientTestSuite(statusCodeSupport, shouldTestNegotiatedSubprotocol, headersTestDelay) { override fun provideClient(): WebSocketClient = KtorWebSocketClient( HttpClient(provideEngine()) { install(WebSockets) }, diff --git a/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorMppWebSocketClientTest.kt b/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorMppWebSocketClientTest.kt index d54952fba..da471cd96 100644 --- a/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorMppWebSocketClientTest.kt +++ b/krossbow-websocket-ktor/src/commonTest/kotlin/org.hildan.krossbow.websocket.ktor/KtorMppWebSocketClientTest.kt @@ -4,6 +4,7 @@ import io.ktor.client.* import io.ktor.client.plugins.websocket.* import org.hildan.krossbow.websocket.* import org.hildan.krossbow.websocket.test.* +import kotlin.time.Duration.Companion.milliseconds private val Platform.statusCodeSupport: StatusCodeSupport get() = when (this) { @@ -26,6 +27,8 @@ class KtorMppWebSocketClientTest : WebSocketClientTestSuite( // Just to be sure we don't attempt to test this with the Java or JS engines // See https://youtrack.jetbrains.com/issue/KTOR-6970 shouldTestNegotiatedSubprotocol = false, + // workaround for https://youtrack.jetbrains.com/issue/KTOR-6883 (NOT fixed for WASM) + headersTestDelay = 200.milliseconds.takeIf { currentPlatform() == Platform.WasmJs.NodeJs }, ) { override fun provideClient(): WebSocketClient = KtorWebSocketClient( HttpClient { diff --git a/krossbow-websocket-ktor/src/wasmJsTest/kotlin/org/hildan/krossbow/websocket/ktor/KtorWasmJsWebSocketClientTest.kt b/krossbow-websocket-ktor/src/wasmJsTest/kotlin/org/hildan/krossbow/websocket/ktor/KtorWasmJsWebSocketClientTest.kt index e69de29bb..4d4be1be6 100644 --- a/krossbow-websocket-ktor/src/wasmJsTest/kotlin/org/hildan/krossbow/websocket/ktor/KtorWasmJsWebSocketClientTest.kt +++ b/krossbow-websocket-ktor/src/wasmJsTest/kotlin/org/hildan/krossbow/websocket/ktor/KtorWasmJsWebSocketClientTest.kt @@ -0,0 +1,17 @@ +package org.hildan.krossbow.websocket.ktor + +import io.ktor.client.engine.* +import io.ktor.client.engine.js.* +import org.hildan.krossbow.websocket.test.* +import kotlin.time.Duration.Companion.milliseconds + +class KtorWasmJsWebSocketClientTest : KtorClientTestSuite( + // The browser cannot reveal status codes for security reasons + statusCodeSupport = if (currentPlatform() is Platform.WasmJs.Browser) StatusCodeSupport.None else StatusCodeSupport.All, + // See https://youtrack.jetbrains.com/issue/KTOR-6970 + shouldTestNegotiatedSubprotocol = false, + // workaround for https://youtrack.jetbrains.com/issue/KTOR-6883 (NOT fixed for WASM) + headersTestDelay = 200.milliseconds.takeIf { currentPlatform() == Platform.WasmJs.NodeJs }, +) { + override fun provideEngine(): HttpClientEngineFactory<*> = Js +} diff --git a/krossbow-websocket-test/src/commonMain/kotlin/org/hildan/krossbow/websocket/test/WebSocketClientTestSuite.kt b/krossbow-websocket-test/src/commonMain/kotlin/org/hildan/krossbow/websocket/test/WebSocketClientTestSuite.kt index d9185bd0f..25690c819 100644 --- a/krossbow-websocket-test/src/commonMain/kotlin/org/hildan/krossbow/websocket/test/WebSocketClientTestSuite.kt +++ b/krossbow-websocket-test/src/commonMain/kotlin/org/hildan/krossbow/websocket/test/WebSocketClientTestSuite.kt @@ -26,6 +26,7 @@ sealed interface StatusCodeSupport { abstract class WebSocketClientTestSuite( private val statusCodeSupport: StatusCodeSupport = StatusCodeSupport.All, private val shouldTestNegotiatedSubprotocol: Boolean = true, + private val headersTestDelay: Duration? = null, ) { abstract fun provideClient(): WebSocketClient @@ -205,8 +206,14 @@ abstract class WebSocketClientTestSuite( @Test fun testHandshakeSubprotocolHeader_noProtocol() = runTestRealTime { + // workaround for https://youtrack.jetbrains.com/issue/KTOR-6883 + val extraParams = if (headersTestDelay != null) mapOf("scheduleDelay" to headersTestDelay.toString()) else emptyMap() val connection = wsClient.connect( - url = testUrl(path = "/sendHandshakeHeaders", testCaseName = "testHandshakeSubprotocolHeader_noProtocol"), + url = testUrl( + path = "/sendHandshakeHeaders", + testCaseName = "testHandshakeSubprotocolHeader_noProtocol", + otherParams = extraParams, + ), protocols = emptyList(), ) try { @@ -221,8 +228,14 @@ abstract class WebSocketClientTestSuite( @Test fun testHandshakeSubprotocolHeader_singleProtocol() = runTestRealTime { + // workaround for https://youtrack.jetbrains.com/issue/KTOR-6883 + val extraParams = if (headersTestDelay != null) mapOf("scheduleDelay" to headersTestDelay.toString()) else emptyMap() val connection = wsClient.connect( - url = testUrl(path = "/sendHandshakeHeaders", testCaseName = "testHandshakeSubprotocolHeader_singleProtocol"), + url = testUrl( + path = "/sendHandshakeHeaders", + testCaseName = "testHandshakeSubprotocolHeader_singleProtocol", + otherParams = extraParams, + ), protocols = listOf("v12.stomp"), ) try { @@ -239,8 +252,14 @@ abstract class WebSocketClientTestSuite( @Test fun testHandshakeSubprotocolHeader_multipleProtocols() = runTestRealTime { + // workaround for https://youtrack.jetbrains.com/issue/KTOR-6883 + val extraParams = if (headersTestDelay != null) mapOf("scheduleDelay" to headersTestDelay.toString()) else emptyMap() val connection = wsClient.connect( - url = testUrl(path = "/sendHandshakeHeaders", testCaseName = "testHandshakeSubprotocolHeader_singleProtocol"), + url = testUrl( + path = "/sendHandshakeHeaders", + testCaseName = "testHandshakeSubprotocolHeader_multipleProtocols", + otherParams = extraParams, + ), protocols = listOf("unknown-protocol", "v12.stomp", "v11.stomp", "v10.stomp"), ) try { @@ -263,14 +282,20 @@ abstract class WebSocketClientTestSuite( fun testHandshakeCustomHeaders() = runTestRealTime { if (wsClient.supportsCustomHeaders) { println("Connecting with agent $agent to ${testServerConfig.wsUrl}/sendHandshakeHeaders") + // workaround for https://youtrack.jetbrains.com/issue/KTOR-6883 + val extraParams = if (headersTestDelay != null) mapOf("scheduleDelay" to headersTestDelay.toString()) else emptyMap() val connection = wsClient.connect( - url = testUrl(path = "/sendHandshakeHeaders", testCaseName = "testHandshakeCustomHeaders"), + url = testUrl( + path = "/sendHandshakeHeaders", + testCaseName = "testHandshakeCustomHeaders", + otherParams = extraParams, + ), headers = mapOf("My-Header-1" to "my-value-1", "My-Header-2" to "my-value-2"), ) println("Connected with agent $agent to ${testServerConfig.wsUrl}/sendHandshakeHeaders") try { // for some reason, this can be pretty long with the Ktor/JS client in nodeJS tests on macOS - val echoedHeadersFrame = connection.expectTextFrame("header info frame") + val echoedHeadersFrame = connection.expectTextFrame("header info frame", 50.seconds) val headers = echoedHeadersFrame.text.lines() assertContains(headers, "My-Header-1=my-value-1") assertContains(headers, "My-Header-2=my-value-2") @@ -332,7 +357,7 @@ private fun runTestRealTime( testBody: suspend CoroutineScope.() -> Unit, ) = runTest(timeout = timeout) { // Switches to a regular dispatcher to avoid the virtual time from runTest. - // We also use limitedParallelism to keep things deterministic + // We also use limitedParallelism to keep things deterministic withContext(Dispatchers.Default.limitedParallelism(1) + context) { testBody() } diff --git a/test-server/websocket-test-server/src/main/kotlin/org/hildan/krossbow/test/server/EchoWebSocketServer.kt b/test-server/websocket-test-server/src/main/kotlin/org/hildan/krossbow/test/server/EchoWebSocketServer.kt index 640d1ae53..b596adf27 100644 --- a/test-server/websocket-test-server/src/main/kotlin/org/hildan/krossbow/test/server/EchoWebSocketServer.kt +++ b/test-server/websocket-test-server/src/main/kotlin/org/hildan/krossbow/test/server/EchoWebSocketServer.kt @@ -8,6 +8,7 @@ import org.java_websocket.protocols.* import org.java_websocket.server.* import java.net.* import java.nio.* +import kotlin.time.Duration private val Draft6455Default = Draft_6455() private val Draft6455WithStomp12 = Draft_6455(emptyList(), listOf(Protocol("v12.stomp"))) @@ -16,20 +17,34 @@ internal class EchoWebSocketServer(port: Int = 0) : WebSocketServer( InetSocketAddress(port), listOf(Draft6455WithStomp12, Draft6455Default), ) { + private val delayedHeadersScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + override fun onStart() { } override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { val uri = URI.create(handshake.resourceDescriptor) if (uri.path == "/sendHandshakeHeaders") { - conn.sendMessageWithHeaders(handshake) + val queryParams = uri.queryAsMap() + val scheduleDelay = queryParams["scheduleDelay"]?.let(Duration::parse) + conn.sendMessageWithHeaders(handshake, scheduleDelay) } } - private fun WebSocket.sendMessageWithHeaders(handshake: ClientHandshake) { + private fun WebSocket.sendMessageWithHeaders(handshake: ClientHandshake, scheduleDelay: Duration? = null) { val headerNames = handshake.iterateHttpFields().asSequence().toList() val headersData = headerNames.joinToString("\n") { "$it=${handshake.getFieldValue(it)}" } - send(headersData) + if (scheduleDelay != null) { + // necessary due to https://youtrack.jetbrains.com/issue/KTOR-6883 + println("Scheduling message with headers in $scheduleDelay") + delayedHeadersScope.launch { + delay(scheduleDelay) + send(headersData) + println("Headers frame sent!") + } + } else { + send(headersData) + } } override fun onMessage(conn: WebSocket, message: String?) { @@ -59,4 +74,13 @@ internal class EchoWebSocketServer(port: Int = 0) : WebSocketServer( } port } + + override fun stop(timeout: Int, closeMessage: String?) { + super.stop(timeout, closeMessage) + delayedHeadersScope.cancel() + } } + +private fun URI.queryAsMap() = query.split("&") + .map { it.split("=") } + .associate { it[0] to it[1] }