193 lines
7.1 KiB
Kotlin
193 lines
7.1 KiB
Kotlin
@file:JvmName("KotlinDartWebKt")
|
|
|
|
package com.tokilabs.proto_socket.crosstest
|
|
|
|
import com.tokilabs.proto_socket.ParserMap
|
|
import com.tokilabs.proto_socket.WsClient
|
|
import com.tokilabs.proto_socket.WsServer
|
|
import com.tokilabs.proto_socket.addRequestListenerTyped
|
|
import com.tokilabs.proto_socket.typeNameOf
|
|
import com.tokilabs.proto_socket.packets.TestData
|
|
import kotlinx.coroutines.CompletableDeferred
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.coroutineScope
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.runBlocking
|
|
import kotlinx.coroutines.withContext
|
|
import kotlinx.coroutines.withTimeoutOrNull
|
|
import java.io.File
|
|
import java.security.KeyFactory
|
|
import java.security.KeyStore
|
|
import java.security.cert.CertificateFactory
|
|
import java.security.cert.X509Certificate
|
|
import java.security.spec.PKCS8EncodedKeySpec
|
|
import java.util.Base64
|
|
import java.util.concurrent.TimeUnit
|
|
import javax.net.ssl.KeyManagerFactory
|
|
import javax.net.ssl.SSLContext
|
|
import kotlin.system.exitProcess
|
|
|
|
private const val HOST = "127.0.0.1"
|
|
private const val WS_PORT = 29498
|
|
private const val WSS_PORT = 29499
|
|
private const val WS_PATH = "/"
|
|
private const val PROCESS_TIMEOUT_MS = 60_000L
|
|
private const val SERVER_OBSERVATION_MS = 500L
|
|
private const val STREAM_JOIN_TIMEOUT_MS = 5_000L
|
|
|
|
fun main() = runBlocking {
|
|
println("INFO typeName kotlin=${typeNameOf<TestData>()}")
|
|
try {
|
|
runWebSuites()
|
|
println("PASS scenario=ws-send-push")
|
|
println("PASS scenario=ws-request-response")
|
|
println("PASS scenario=wss-send-push")
|
|
println("PASS scenario=wss-request-response")
|
|
println("PASS all kotlin-server/dart-web-client crosstests passed")
|
|
} catch (error: Throwable) {
|
|
System.err.println("FAIL crosstest error=${error.message ?: error}")
|
|
exitProcess(1)
|
|
}
|
|
}
|
|
|
|
private suspend fun runWebSuites() = coroutineScope {
|
|
val wsReceived = CompletableDeferred<Boolean>()
|
|
val wssReceived = CompletableDeferred<Boolean>()
|
|
val certFile = kotlinResourceFile("server.crt")
|
|
val keyFile = kotlinResourceFile("server.key")
|
|
val serverCtx = buildServerSslContext(certFile.path, keyFile.path)
|
|
val wsServer = webServer(WS_PORT, null, wsReceived)
|
|
val wssServer = webServer(WSS_PORT, serverCtx, wssReceived)
|
|
|
|
wsServer.start()
|
|
wssServer.start()
|
|
delay(100)
|
|
try {
|
|
runDartBrowserTest("test/browser_ws_kotlin_test.dart")
|
|
expectReceived("WS", wsReceived)
|
|
expectReceived("WSS", wssReceived)
|
|
} finally {
|
|
wssServer.stop()
|
|
wsServer.stop()
|
|
}
|
|
}
|
|
|
|
private fun webServer(
|
|
port: Int,
|
|
sslContext: SSLContext?,
|
|
received: CompletableDeferred<Boolean>,
|
|
): WsServer {
|
|
val server = WsServer(HOST, port, WS_PATH, sslContext = sslContext) { conn ->
|
|
WsClient.forServer(conn, 0, 0, parserMap())
|
|
}
|
|
server.onClientConnected = { client ->
|
|
addRequestListenerTyped<TestData, TestData>(client.communicator) { req ->
|
|
println("SERVER_RECEIVED index=${req.index} message=${req.message}")
|
|
if (req.index == 101 && req.message == "fire from dart web client") {
|
|
if (!received.isCompleted) received.complete(true)
|
|
runBlocking {
|
|
client.send(
|
|
TestData.newBuilder()
|
|
.setIndex(200)
|
|
.setMessage("push from kotlin server")
|
|
.build(),
|
|
)
|
|
}
|
|
}
|
|
TestData.newBuilder()
|
|
.setIndex(req.index * 2)
|
|
.setMessage("echo: ${req.message}")
|
|
.build()
|
|
}
|
|
}
|
|
return server
|
|
}
|
|
|
|
private suspend fun expectReceived(
|
|
transport: String,
|
|
received: CompletableDeferred<Boolean>,
|
|
) {
|
|
val ok = withTimeoutOrNull(SERVER_OBSERVATION_MS) { received.await() } ?: false
|
|
check(ok) { "$transport web send-push server did not receive expected data" }
|
|
}
|
|
|
|
private fun destroyProcessTree(process: Process) {
|
|
process.toHandle().descendants().forEach { it.destroyForcibly() }
|
|
process.destroyForcibly()
|
|
}
|
|
|
|
private suspend fun runDartBrowserTest(testPath: String) = withContext(Dispatchers.IO) {
|
|
val process = ProcessBuilder("dart", "test", "-p", "chrome", testPath)
|
|
.directory(dartDir())
|
|
.redirectErrorStream(false)
|
|
.start()
|
|
|
|
val stdoutThread = Thread {
|
|
process.inputStream.bufferedReader().forEachLine { line -> println(line) }
|
|
}
|
|
val stderrThread = Thread {
|
|
process.errorStream.bufferedReader().forEachLine { line -> System.err.println(line) }
|
|
}
|
|
stdoutThread.start()
|
|
stderrThread.start()
|
|
|
|
val finished = process.waitFor(PROCESS_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
|
if (!finished) {
|
|
destroyProcessTree(process)
|
|
}
|
|
stdoutThread.join(STREAM_JOIN_TIMEOUT_MS)
|
|
stderrThread.join(STREAM_JOIN_TIMEOUT_MS)
|
|
check(!stdoutThread.isAlive && !stderrThread.isAlive) {
|
|
"dart browser test stream readers did not finish"
|
|
}
|
|
val exitCode = if (finished) process.exitValue() else -1
|
|
check(exitCode == 0) { "dart browser test failed exitCode=$exitCode" }
|
|
}
|
|
|
|
private fun parserMap(): ParserMap =
|
|
mapOf(typeNameOf<TestData>() to { TestData.parseFrom(it) })
|
|
|
|
private fun buildServerSslContext(certPath: String, keyPath: String): SSLContext {
|
|
val certFactory = CertificateFactory.getInstance("X.509")
|
|
val cert = File(certPath).inputStream().use {
|
|
certFactory.generateCertificate(it) as X509Certificate
|
|
}
|
|
val keyPem = File(keyPath).readText()
|
|
.replace("-----BEGIN PRIVATE KEY-----", "")
|
|
.replace("-----END PRIVATE KEY-----", "")
|
|
.replace("\\s+".toRegex(), "")
|
|
val privateKey = KeyFactory.getInstance("RSA")
|
|
.generatePrivate(PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyPem)))
|
|
val keyStore = KeyStore.getInstance("PKCS12")
|
|
keyStore.load(null, null)
|
|
keyStore.setKeyEntry("toki", privateKey, CharArray(0), arrayOf(cert))
|
|
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
|
keyManagerFactory.init(keyStore, CharArray(0))
|
|
val ctx = SSLContext.getInstance("TLS")
|
|
ctx.init(keyManagerFactory.keyManagers, null, null)
|
|
return ctx
|
|
}
|
|
|
|
private fun kotlinResourceFile(name: String): File {
|
|
var dir = File(System.getProperty("user.dir")).absoluteFile
|
|
while (dir.parentFile != null) {
|
|
val direct = File(dir, "src/test/resources/$name").canonicalFile
|
|
if (direct.isFile) return direct
|
|
val nested = File(dir, "kotlin/src/test/resources/$name").canonicalFile
|
|
if (nested.isFile) return nested
|
|
dir = dir.parentFile
|
|
}
|
|
error("cannot resolve kotlin test resource $name")
|
|
}
|
|
|
|
private fun dartDir(): File {
|
|
var dir = File(System.getProperty("user.dir")).absoluteFile
|
|
while (dir.parentFile != null) {
|
|
val candidate = File(dir, "../dart").canonicalFile
|
|
if (File(candidate, "pubspec.yaml").isFile) return candidate
|
|
val direct = File(dir, "dart").canonicalFile
|
|
if (File(direct, "pubspec.yaml").isFile) return direct
|
|
dir = dir.parentFile
|
|
}
|
|
error("cannot resolve dart directory")
|
|
}
|