Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[apollo-mockserver] cosmetics and add listener #5360

Merged
merged 2 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading