com.hexagonkt.http.test.examples.WebSocketsTest.kt Maven / Gradle / Ivy
package com.hexagonkt.http.test.examples
import com.hexagonkt.core.logging.logger
import com.hexagonkt.core.require
import com.hexagonkt.core.urlOf
import com.hexagonkt.http.SslSettings
import com.hexagonkt.http.client.HttpClient
import com.hexagonkt.http.client.HttpClientPort
import com.hexagonkt.http.client.HttpClientSettings
import com.hexagonkt.http.model.HttpProtocol.*
import com.hexagonkt.http.model.ws.NORMAL
import com.hexagonkt.http.model.ws.WsSession
import com.hexagonkt.http.server.*
import com.hexagonkt.http.handlers.HttpHandler
import com.hexagonkt.http.handlers.path
import com.hexagonkt.http.test.BaseTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.OS.WINDOWS
import kotlin.IllegalStateException
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
@Suppress("FunctionName") // This class's functions are intended to be used only in tests
abstract class WebSocketsTest(
final override val clientAdapter: () -> HttpClientPort,
final override val serverAdapter: () -> HttpServerPort,
final override val serverSettings: HttpServerSettings = HttpServerSettings(),
) : BaseTest() {
private val identity = "hexagontk.p12"
private val trust = "trust.p12"
private val keyStore = urlOf("classpath:ssl/$identity")
private val trustStore = urlOf("classpath:ssl/$trust")
private val keyStorePassword = identity.reversed()
private val trustStorePassword = trust.reversed()
private val sslSettings = SslSettings(
keyStore = keyStore,
keyStorePassword = keyStorePassword,
trustStore = trustStore,
trustStorePassword = trustStorePassword,
clientAuth = true
)
private val http2ServerSettings = serverSettings.copy(
bindPort = 0,
protocol = HTTP2,
sslSettings = sslSettings
)
// TODO Add WebSockets samples: auth, request access, store sessions, close sessions...
private val clientSettings = HttpClientSettings(sslSettings = sslSettings)
// ws_server
private var sessions = emptyMap>()
override val handler: HttpHandler = path {
ws("/ws/{id}") {
// Path parameters can also be accessed on WS handlers like `onText`
val idParameter = pathParameters.require("id")
// Request is handled like other HTTP methods, if errors, no WS connection is made
val id = idParameter.toIntOrNull()
?: return@ws badRequest("ID must be a number: $idParameter")
// Accepted returns the callbacks to handle WS requests
accepted(
// All callbacks have their session as the receiver
onConnect = {
val se = sessions[id] ?: emptyList()
sessions = sessions + (id to se + this)
},
onBinary = { bytes ->
if (bytes.isEmpty()) {
// The HTTP request data can be queried from the WS session
val certificateSubject = request.certificate()?.subjectX500Principal?.name
send(certificateSubject?.toByteArray() ?: byteArrayOf())
}
},
onText = { text ->
val se = sessions[id] ?: emptyList()
for (s in se)
// It is allowed to send data on previously stored sessions
s.send(text)
},
// Ping requests helps to maintain WS sessions opened
onPing = { bytes -> pong(bytes) },
// Pong handlers should be used to check sent pings
onPong = { bytes -> send(bytes) },
// Callback executed when WS sessions are closed (on the server or client side)
onClose = { status, reason ->
logger.info { "$status: $reason" }
val se = sessions[id] ?: emptyList()
sessions = sessions + (id to se - this)
}
)
}
}
// ws_server
@Test fun `WebSockets client check start and stop states`() {
val settings = clientSettings.copy(baseUrl = urlOf("https://localhost:9999"))
val client = HttpClient(clientAdapter(), settings)
assertEquals(
"HTTP client *MUST BE STARTED* before connecting to WS",
assertFailsWith { client.ws("/ws/1") }.message
)
assertEquals(
"HTTP client *MUST BE STARTED* before shut-down",
assertFailsWith { client.stop() }.message
)
}
@Test fun `WebSockets connections can be checked before session is created`() {
assertFails {
client.ws(
path = "/ws/a",
onText = {
if (it.lowercase().contains("bye"))
close(NORMAL, "Thanks")
}
)
}
}
@Test fun `Serve WS works properly`() {
wsTest(serverSettings.copy(bindPort = 0), clientSettings)
}
@Test
@DisabledOnOs(WINDOWS) // TODO There are problems with certificates in Windows
fun `Serve WSS works properly`() {
wsTest(http2ServerSettings.copy(protocol = HTTPS), clientSettings)
}
@Test
@DisabledOnOs(WINDOWS) // TODO There are problems with certificates in Windows
fun `Serve WSS over HTTP2 works properly`() {
wsTest(http2ServerSettings, clientSettings)
}
private fun wsTest(
serverSettings: HttpServerSettings,
clientSettings: HttpClientSettings,
) {
val server = serve(serverAdapter(), handler, serverSettings)
val client = HttpClient(clientAdapter(), clientSettings.copy(baseUrl = server.binding))
client.start()
// ws_client
val results = mutableMapOf>()
val ws = client.ws(
path = "/ws/1",
onText = { text ->
synchronized(results) {
results[1] = (results[1] ?: emptySet()) + text
}
},
onClose = { status, reason ->
logger.info { "Closed with: $status - $reason" }
}
)
// ws_client
val ws1 = client.ws(
path = "/ws/1",
onText = {
synchronized(results) {
results[1] = (results[1] ?: emptySet()) + "$it#"
}
if (it.lowercase().contains("bye"))
close(NORMAL, "Thanks")
}
)
val ws2 = client.ws(
path = "/ws/2",
onText = {
results[2] = (results[2] ?: emptySet()) + "$it@"
if (it.lowercase().contains("bye"))
close(NORMAL, "Thanks")
}
)
ws1.send("Hello")
Thread.sleep(300)
assertEquals(setOf("Hello", "Hello#"), results[1])
assertNull(results[2])
ws1.send("Goodbye")
Thread.sleep(300)
ws1.close()
ws.close()
assertEquals(setOf("Hello", "Hello#", "Goodbye", "Goodbye#"), results[1])
assertNull(results[2])
ws2.send("Hello")
Thread.sleep(300)
assertEquals(setOf("Hello@"), results[2])
ws2.send("Goodbye")
Thread.sleep(300)
ws2.close()
assertEquals(setOf("Hello@", "Goodbye@"), results[2])
client.stop()
server.stop()
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy