diff --git a/build.gradle.kts b/build.gradle.kts index 68b9c44..e4380b5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -136,6 +136,8 @@ kotlin { jvmTest.dependencies { implementation(libs.kotest.runner.junit5) runtimeOnly(libs.slf4j.simple) + implementation(libs.ktor.server.websockets) + implementation(libs.ktor.server.test.host) } jsMain.dependencies { implementation(libs.ktor.client.js) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fcc9549..8757743 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,8 @@ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "k ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } +ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" } +ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version = "1.6.2" } diff --git a/src/commonMain/kotlin/ktor/WebsocketLoxoneClient.kt b/src/commonMain/kotlin/ktor/WebsocketLoxoneClient.kt index 040b428..8753512 100644 --- a/src/commonMain/kotlin/ktor/WebsocketLoxoneClient.kt +++ b/src/commonMain/kotlin/ktor/WebsocketLoxoneClient.kt @@ -23,17 +23,20 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.withTimeout +import kotlin.jvm.JvmOverloads -class WebsocketLoxoneClient( - private val endpoint: LoxoneEndpoint, +class WebsocketLoxoneClient internal constructor( + private val client: HttpClient, + private val endpoint: LoxoneEndpoint? = null, private val authenticator: LoxoneTokenAuthenticator? = null ) : LoxoneClient { - private val logger = KotlinLogging.logger {} + @JvmOverloads constructor( + endpoint: LoxoneEndpoint, + authenticator: LoxoneTokenAuthenticator? = null + ) : this(HttpClient { install(WebSockets) }, endpoint, authenticator) - private val client = HttpClient { - install(WebSockets) - } + private val logger = KotlinLogging.logger {} private val webSocketSession = AtomicReference(null) @@ -74,11 +77,11 @@ class WebsocketLoxoneClient( webSocketSession.compareAndSet( null, client.webSocketSession( - host = endpoint.host, - port = endpoint.port, - path = endpoint.path + WS_PATH, + host = endpoint?.host, + port = endpoint?.port, + path = if (endpoint != null) endpoint.path + WS_PATH else WS_PATH, block = { - url.protocol = if (endpoint.useSsl) URLProtocol.WSS else URLProtocol.WS + url.protocol = if (endpoint?.useSsl == true) URLProtocol.WSS else URLProtocol.WS } ) ) diff --git a/src/jvmTest/kotlin/ktor/WebsocketLoxoneClientTest.kt b/src/jvmTest/kotlin/ktor/WebsocketLoxoneClientTest.kt new file mode 100644 index 0000000..45fd0e4 --- /dev/null +++ b/src/jvmTest/kotlin/ktor/WebsocketLoxoneClientTest.kt @@ -0,0 +1,57 @@ +package cz.smarteon.loxone.ktor + +import cz.smarteon.loxone.Codec +import cz.smarteon.loxone.message.ApiInfo +import cz.smarteon.loxone.message.MessageHeader +import cz.smarteon.loxone.message.MessageKind.TEXT +import cz.smarteon.loxone.message.TestingLoxValues.API_INFO_MSG_VAL +import cz.smarteon.loxone.message.TestingMessages.okMsg +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.ktor.client.* +import io.ktor.server.application.* +import io.ktor.server.testing.* +import io.ktor.server.websocket.* +import io.ktor.websocket.* +import kotlinx.coroutines.channels.consumeEach +import io.ktor.client.plugins.websocket.WebSockets as ClientWebsockets +import io.ktor.server.websocket.WebSockets as ServerWebsockets + +class WebsocketLoxoneClientTest : StringSpec({ + + "should call simple command" { + testWebsocket { + val client = WebsocketLoxoneClient(this) + + client.callRaw("jdev/cfg/api") shouldBe okMsg("dev/cfg/api", API_INFO_MSG_VAL) + + val response = client.call(ApiInfo.command) + response.code shouldBe "200" + response.value shouldBe API_INFO_MSG_VAL + response.control shouldBe "dev/cfg/api" + } + } + +}) + +private suspend fun testWebsocket(test: suspend HttpClient.() -> Unit) = testApplication { + application { install(ServerWebsockets) } + + routing { + webSocketRaw(path = "/ws/rfc6455") { + incoming.consumeEach { frame -> + if (frame is Frame.Text) { + when (frame.readText()) { + "jdev/cfg/api" -> { + val text = okMsg("dev/cfg/api", API_INFO_MSG_VAL).encodeToByteArray() + send(Frame.Binary(true, Codec.writeHeader(MessageHeader(TEXT, false, text.size.toLong())))) + send(Frame.Text(true, text)) + } + } + } + } + } + } + + createClient { install(ClientWebsockets) }.use { it.test() } +}