test(android): add bridge unit tests
This commit is contained in:
@@ -76,4 +76,7 @@ dependencies {
|
|||||||
implementation("androidx.camera:camera-lifecycle:1.3.4")
|
implementation("androidx.camera:camera-lifecycle:1.3.4")
|
||||||
implementation("androidx.camera:camera-video:1.3.4")
|
implementation("androidx.camera:camera-video:1.3.4")
|
||||||
implementation("androidx.camera:camera-view:1.3.4")
|
implementation("androidx.camera:camera-view:1.3.4")
|
||||||
|
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.steipete.clawdis.node.bridge
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.BufferedWriter
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.net.ServerSocket
|
||||||
|
|
||||||
|
class BridgePairingClientTest {
|
||||||
|
@Test
|
||||||
|
fun helloOkReturnsExistingToken() = runBlocking {
|
||||||
|
val serverSocket = ServerSocket(0)
|
||||||
|
val port = serverSocket.localPort
|
||||||
|
|
||||||
|
val server =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
serverSocket.use { ss ->
|
||||||
|
val sock = ss.accept()
|
||||||
|
sock.use { s ->
|
||||||
|
val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8))
|
||||||
|
val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8))
|
||||||
|
|
||||||
|
val hello = reader.readLine()
|
||||||
|
assertTrue(hello.contains("\"type\":\"hello\""))
|
||||||
|
writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = BridgePairingClient()
|
||||||
|
val res =
|
||||||
|
client.pairAndHello(
|
||||||
|
endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port),
|
||||||
|
hello =
|
||||||
|
BridgePairingClient.Hello(
|
||||||
|
nodeId = "node-1",
|
||||||
|
displayName = "Android Node",
|
||||||
|
token = "token-123",
|
||||||
|
platform = "Android",
|
||||||
|
version = "test",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertTrue(res.ok)
|
||||||
|
assertEquals("token-123", res.token)
|
||||||
|
server.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun notPairedTriggersPairRequestAndReturnsToken() = runBlocking {
|
||||||
|
val serverSocket = ServerSocket(0)
|
||||||
|
val port = serverSocket.localPort
|
||||||
|
|
||||||
|
val server =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
serverSocket.use { ss ->
|
||||||
|
val sock = ss.accept()
|
||||||
|
sock.use { s ->
|
||||||
|
val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8))
|
||||||
|
val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8))
|
||||||
|
|
||||||
|
reader.readLine() // hello
|
||||||
|
writer.write("""{"type":"error","code":"NOT_PAIRED","message":"not paired"}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
|
||||||
|
val pairReq = reader.readLine()
|
||||||
|
assertTrue(pairReq.contains("\"type\":\"pair-request\""))
|
||||||
|
writer.write("""{"type":"pair-ok","token":"new-token"}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = BridgePairingClient()
|
||||||
|
val res =
|
||||||
|
client.pairAndHello(
|
||||||
|
endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port),
|
||||||
|
hello =
|
||||||
|
BridgePairingClient.Hello(
|
||||||
|
nodeId = "node-1",
|
||||||
|
displayName = "Android Node",
|
||||||
|
token = null,
|
||||||
|
platform = "Android",
|
||||||
|
version = "test",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertTrue(res.ok)
|
||||||
|
assertEquals("new-token", res.token)
|
||||||
|
server.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package com.steipete.clawdis.node.bridge
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.BufferedWriter
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.io.OutputStreamWriter
|
||||||
|
import java.net.ServerSocket
|
||||||
|
|
||||||
|
class BridgeSessionTest {
|
||||||
|
@Test
|
||||||
|
fun requestReturnsPayloadJson() = runBlocking {
|
||||||
|
val serverSocket = ServerSocket(0)
|
||||||
|
val port = serverSocket.localPort
|
||||||
|
|
||||||
|
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
val connected = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
val session =
|
||||||
|
BridgeSession(
|
||||||
|
scope = scope,
|
||||||
|
onConnected = { _, _ -> connected.complete(Unit) },
|
||||||
|
onDisconnected = { /* ignore */ },
|
||||||
|
onEvent = { _, _ -> /* ignore */ },
|
||||||
|
onInvoke = { BridgeSession.InvokeResult.ok(null) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val server =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
serverSocket.use { ss ->
|
||||||
|
val sock = ss.accept()
|
||||||
|
sock.use { s ->
|
||||||
|
val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8))
|
||||||
|
val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8))
|
||||||
|
|
||||||
|
val hello = reader.readLine()
|
||||||
|
assertTrue(hello.contains("\"type\":\"hello\""))
|
||||||
|
writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
|
||||||
|
val req = reader.readLine()
|
||||||
|
assertTrue(req.contains("\"type\":\"req\""))
|
||||||
|
val id = extractJsonString(req, "id")
|
||||||
|
writer.write("""{"type":"res","id":"$id","ok":true,"payloadJSON":"{\"value\":123}"}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.connect(
|
||||||
|
endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port),
|
||||||
|
hello =
|
||||||
|
BridgeSession.Hello(
|
||||||
|
nodeId = "node-1",
|
||||||
|
displayName = "Android Node",
|
||||||
|
token = null,
|
||||||
|
platform = "Android",
|
||||||
|
version = "test",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
connected.await()
|
||||||
|
val payload = session.request(method = "health", paramsJson = null)
|
||||||
|
assertEquals("""{"value":123}""", payload)
|
||||||
|
server.await()
|
||||||
|
|
||||||
|
session.disconnect()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun requestThrowsOnErrorResponse() = runBlocking {
|
||||||
|
val serverSocket = ServerSocket(0)
|
||||||
|
val port = serverSocket.localPort
|
||||||
|
|
||||||
|
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
val connected = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
val session =
|
||||||
|
BridgeSession(
|
||||||
|
scope = scope,
|
||||||
|
onConnected = { _, _ -> connected.complete(Unit) },
|
||||||
|
onDisconnected = { /* ignore */ },
|
||||||
|
onEvent = { _, _ -> /* ignore */ },
|
||||||
|
onInvoke = { BridgeSession.InvokeResult.ok(null) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val server =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
serverSocket.use { ss ->
|
||||||
|
val sock = ss.accept()
|
||||||
|
sock.use { s ->
|
||||||
|
val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8))
|
||||||
|
val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8))
|
||||||
|
|
||||||
|
reader.readLine() // hello
|
||||||
|
writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
|
||||||
|
val req = reader.readLine()
|
||||||
|
val id = extractJsonString(req, "id")
|
||||||
|
writer.write(
|
||||||
|
"""{"type":"res","id":"$id","ok":false,"error":{"code":"FORBIDDEN","message":"nope"}}""",
|
||||||
|
)
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.connect(
|
||||||
|
endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port),
|
||||||
|
hello =
|
||||||
|
BridgeSession.Hello(
|
||||||
|
nodeId = "node-1",
|
||||||
|
displayName = "Android Node",
|
||||||
|
token = null,
|
||||||
|
platform = "Android",
|
||||||
|
version = "test",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
connected.await()
|
||||||
|
|
||||||
|
try {
|
||||||
|
session.request(method = "chat.history", paramsJson = """{"sessionKey":"main"}""")
|
||||||
|
throw AssertionError("expected request() to throw")
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
assertTrue(e.message?.contains("FORBIDDEN: nope") == true)
|
||||||
|
}
|
||||||
|
server.await()
|
||||||
|
|
||||||
|
session.disconnect()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun invokeResReturnsErrorWhenHandlerThrows() = runBlocking {
|
||||||
|
val serverSocket = ServerSocket(0)
|
||||||
|
val port = serverSocket.localPort
|
||||||
|
|
||||||
|
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
val connected = CompletableDeferred<Unit>()
|
||||||
|
|
||||||
|
val session =
|
||||||
|
BridgeSession(
|
||||||
|
scope = scope,
|
||||||
|
onConnected = { _, _ -> connected.complete(Unit) },
|
||||||
|
onDisconnected = { /* ignore */ },
|
||||||
|
onEvent = { _, _ -> /* ignore */ },
|
||||||
|
onInvoke = { throw IllegalStateException("FOO_BAR: boom") },
|
||||||
|
)
|
||||||
|
|
||||||
|
val invokeResLine = CompletableDeferred<String>()
|
||||||
|
val server =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
serverSocket.use { ss ->
|
||||||
|
val sock = ss.accept()
|
||||||
|
sock.use { s ->
|
||||||
|
val reader = BufferedReader(InputStreamReader(s.getInputStream(), Charsets.UTF_8))
|
||||||
|
val writer = BufferedWriter(OutputStreamWriter(s.getOutputStream(), Charsets.UTF_8))
|
||||||
|
|
||||||
|
reader.readLine() // hello
|
||||||
|
writer.write("""{"type":"hello-ok","serverName":"Test Bridge"}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
|
||||||
|
// Ask the node to invoke something; handler will throw.
|
||||||
|
writer.write("""{"type":"invoke","id":"i1","command":"screen.snapshot","paramsJSON":null}""")
|
||||||
|
writer.write("\n")
|
||||||
|
writer.flush()
|
||||||
|
|
||||||
|
val res = reader.readLine()
|
||||||
|
invokeResLine.complete(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.connect(
|
||||||
|
endpoint = BridgeEndpoint.manual(host = "127.0.0.1", port = port),
|
||||||
|
hello =
|
||||||
|
BridgeSession.Hello(
|
||||||
|
nodeId = "node-1",
|
||||||
|
displayName = "Android Node",
|
||||||
|
token = null,
|
||||||
|
platform = "Android",
|
||||||
|
version = "test",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
connected.await()
|
||||||
|
|
||||||
|
// Give the reader loop time to process.
|
||||||
|
val line = invokeResLine.await()
|
||||||
|
assertTrue(line.contains("\"type\":\"invoke-res\""))
|
||||||
|
assertTrue(line.contains("\"ok\":false"))
|
||||||
|
assertTrue(line.contains("\"code\":\"FOO_BAR\""))
|
||||||
|
assertTrue(line.contains("\"message\":\"boom\""))
|
||||||
|
server.await()
|
||||||
|
|
||||||
|
session.disconnect()
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractJsonString(raw: String, key: String): String {
|
||||||
|
val needle = "\"$key\":\""
|
||||||
|
val start = raw.indexOf(needle)
|
||||||
|
if (start < 0) throw IllegalArgumentException("missing key $key in $raw")
|
||||||
|
val from = start + needle.length
|
||||||
|
val end = raw.indexOf('"', from)
|
||||||
|
if (end < 0) throw IllegalArgumentException("unterminated string for $key in $raw")
|
||||||
|
return raw.substring(from, end)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user