Skip to content

Commit

Permalink
[apollo-mockserver] cosmetics and add listener (#5360)
Browse files Browse the repository at this point in the history
* add listeners to MockServer

* update API dump
  • Loading branch information
martinbonnin authored Nov 8, 2023
1 parent 4c8b80f commit 5100819
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 20 deletions.
6 changes: 6 additions & 0 deletions libraries/apollo-mockserver/api/apollo-mockserver.api
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ public final class com/apollographql/apollo3/mockserver/MockServer$Builder {
public final fun build ()Lcom/apollographql/apollo3/mockserver/MockServer;
public final fun handlePings (Z)Lcom/apollographql/apollo3/mockserver/MockServer$Builder;
public final fun handler (Lcom/apollographql/apollo3/mockserver/MockServerHandler;)Lcom/apollographql/apollo3/mockserver/MockServer$Builder;
public final fun listener (Lcom/apollographql/apollo3/mockserver/MockServer$Listener;)Lcom/apollographql/apollo3/mockserver/MockServer$Builder;
public final fun tcpServer (Lcom/apollographql/apollo3/mockserver/TcpServer;)Lcom/apollographql/apollo3/mockserver/MockServer$Builder;
}

public abstract interface class com/apollographql/apollo3/mockserver/MockServer$Listener {
public abstract fun onMessage (Lcom/apollographql/apollo3/mockserver/WebSocketMessage;)V
public abstract fun onRequest (Lcom/apollographql/apollo3/mockserver/MockRequestBase;)V
}

public abstract interface class com/apollographql/apollo3/mockserver/MockServerHandler {
public abstract fun handle (Lcom/apollographql/apollo3/mockserver/MockRequestBase;)Lcom/apollographql/apollo3/mockserver/MockResponse;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ sealed interface WebSocketMessage
@ApolloExperimental
class TextMessage(val text: String) : WebSocketMessage
@ApolloExperimental
class BinaryMessage(val bytes: ByteArray) : WebSocketMessage
class DataMessage(val data: ByteArray) : WebSocketMessage
@ApolloExperimental
class CloseFrame(val code: Int?, val reason: String?) : WebSocketMessage
@ApolloExperimental
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,24 @@ import kotlin.js.JsName
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

/**
* A server for testing Kotlin Multiplatform applications using HTTP and WebSockets.
*
* A [MockServer] binds to localhost and allows to enqueue predefined responses using [enqueue], [enqueueString],
* [enqueueMultipart] and [enqueueWebSocket]
*
* [MockServer] is very simple and should not be used for production applications. HTTPS is a non-goal as well as
* performance.
* Also, [MockServer] makes no attempt at flow control:
* - data is read as fast as possible from the network and buffered until [takeRequest] or [awaitAnyRequest] is called.
* - queued responses from [enqueue] are buffered until they can be transmitted to the network.
* If you're using [MockServer] to handle large payloads, it will use a lot of memory.
*/
interface MockServer : Closeable {
/**
* Returns the root url for this server
* Returns the url for this server in the form "http://ip:port/".
*
* It will suspend until a port is found to listen to
* This function is suspend because finding an available port is an asynchronous operation on some platforms.
*/
suspend fun url(): String

Expand All @@ -34,34 +47,40 @@ interface MockServer : Closeable {
/**
* Closes the server.
*
* The locally bound address is freed immediately
* Active connections might stay alive after this call but will eventually terminate
* The locally bound socket listening to new connections is freed immediately.
* Active connections might stay alive after this call but will eventually terminate.
*/
override fun close()

/**
* Enqueue a response
* Enqueue a response.
*/
fun enqueue(mockResponse: MockResponse)

/**
* Return a request from the recorded requests or throw if no request has been received
* Return a request from the recorded requests or throw if no request has been received.
*
* @see [awaitRequest] and [awaitWebSocketRequest]
*/
fun takeRequest(): MockRequest

/**
* Wait for a request and return it
* Wait for a request and return it.
*
* @see [awaitRequest] and [awaitWebSocketRequest]
*/
suspend fun awaitAnyRequest(timeout: Duration = 1.seconds): MockRequestBase

interface Listener {
fun onRequest(request: MockRequestBase)
fun onMessage(message: WebSocketMessage)
}

class Builder {
private var handler: MockServerHandler? = null
private var handlePings: Boolean? = null
private var tcpServer: TcpServer? = null
private var listener: Listener? = null

fun handler(handler: MockServerHandler) = apply {
this.handler = handler
Expand All @@ -75,11 +94,17 @@ interface MockServer : Closeable {
this.tcpServer = tcpServer
}

fun listener(listener: Listener) = apply {
this.listener = listener
}


fun build(): MockServer {
return MockServerImpl(
handler ?: QueueMockServerHandler(),
handlePings ?: true,
tcpServer ?: TcpServer()
tcpServer ?: TcpServer(),
listener
)
}
}
Expand All @@ -89,6 +114,7 @@ internal class MockServerImpl(
private val mockServerHandler: MockServerHandler,
private val handlePings: Boolean,
private val server: TcpServer,
private val listener: MockServer.Listener?
) : MockServer {
private val requests = Channel<MockRequestBase>(Channel.UNLIMITED)
private val scope = CoroutineScope(SupervisorJob())
Expand All @@ -101,7 +127,7 @@ internal class MockServerImpl(
scope.launch {
//println("Socket bound: ${url()}")
try {
handleRequests(mockServerHandler, socket) {
handleRequests(mockServerHandler, socket, listener) {
requests.trySend(it)
}
} catch (e: Exception) {
Expand All @@ -128,7 +154,7 @@ internal class MockServerImpl(
}
}

private suspend fun handleRequests(handler: MockServerHandler, socket: TcpSocket, onRequest: (MockRequestBase) -> Unit) {
private suspend fun handleRequests(handler: MockServerHandler, socket: TcpSocket, listener: MockServer.Listener?, onRequest: (MockRequestBase) -> Unit) {
val buffer = Buffer()
val reader = object : Reader {
override val buffer: Buffer
Expand All @@ -142,6 +168,8 @@ internal class MockServerImpl(

while (true) {
val request = readRequest(reader)
listener?.onRequest(request)

onRequest(request)

val response = handler.handle(request)
Expand All @@ -152,6 +180,7 @@ internal class MockServerImpl(
if (request is WebsocketMockRequest) {
launch {
readFrames(reader) { message ->
listener?.onMessage(message)
when {
handlePings && message is PingFrame -> {
socket.send(pongFrame())
Expand Down Expand Up @@ -206,11 +235,11 @@ internal class MockServerImpl(
}

@JsName("createMockServer")
fun MockServer(): MockServer = MockServerImpl(QueueMockServerHandler(), true, TcpServer())
fun MockServer(): MockServer = MockServerImpl(QueueMockServerHandler(), true, TcpServer(), null)

@Deprecated("Use MockServer.Builder() instead", level = DeprecationLevel.ERROR)
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_0)
fun MockServer(handler: MockServerHandler): MockServer = MockServerImpl(handler, true, TcpServer())
fun MockServer(handler: MockServerHandler): MockServer = MockServerImpl(handler, true, TcpServer(), null)

@Deprecated("Use enqueueString instead", ReplaceWith("enqueueString"), DeprecationLevel.ERROR)
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v4_0_0)
Expand Down Expand Up @@ -265,12 +294,13 @@ interface WebSocketBody {

@ApolloExperimental
fun MockServer.enqueueWebSocket(
statusCode: Int = 101,
headers: Map<String, String> = emptyMap(),
): WebSocketBody {
val webSocketBody = WebSocketBodyImpl()
enqueue(
MockResponse.Builder()
.statusCode(101)
.statusCode(statusCode)
.body(webSocketBody.consumeAsFlow())
.headers(headers)
.addHeader("Upgrade", "websocket")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ internal suspend fun readFrames(reader: Reader, onMessage: (WebSocketMessage) ->
OPCODE_BINARY -> {
currentMessage.write(payload, payloadLength)
if (fin) {
onMessage(BinaryMessage(currentMessage.readByteArray()))
onMessage(DataMessage(currentMessage.readByteArray()))
currentOpcode = null
} else {
currentOpcode = opcode
Expand Down Expand Up @@ -226,7 +226,7 @@ private fun WebSocketMessage.toFrame(): ByteString {
is PingFrame -> pingFrame().toByteString()
is CloseFrame -> closeFrame(code, reason)
is TextMessage -> textFrame(text)
is BinaryMessage -> binaryFrame(bytes)
is DataMessage -> binaryFrame(data)
}
}

Expand Down
8 changes: 4 additions & 4 deletions tests/engine/src/commonTest/kotlin/WebSocketEngineTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import com.apollographql.apollo3.api.http.HttpHeader
import com.apollographql.apollo3.exception.ApolloException
import com.apollographql.apollo3.exception.ApolloNetworkException
import com.apollographql.apollo3.exception.ApolloWebSocketClosedException
import com.apollographql.apollo3.mockserver.BinaryMessage
import com.apollographql.apollo3.mockserver.DataMessage
import com.apollographql.apollo3.mockserver.CloseFrame
import com.apollographql.apollo3.mockserver.MockServer
import com.apollographql.apollo3.mockserver.TextMessage
Expand Down Expand Up @@ -72,11 +72,11 @@ class WebSocketEngineTest {

connection.send("client->server".encodeUtf8())
request.awaitMessage().apply {
assertIs<BinaryMessage>(this)
assertEquals("client->server", bytes.decodeToString())
assertIs<DataMessage>(this)
assertEquals("client->server", data.decodeToString())
}

responseBody.enqueueMessage(BinaryMessage("server->client".encodeToByteArray()))
responseBody.enqueueMessage(DataMessage("server->client".encodeToByteArray()))
assertEquals("server->client", connection.receive())

connection.close()
Expand Down

0 comments on commit 5100819

Please sign in to comment.