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

MockServer improvements and TestNetworkTransport #3757

Merged
merged 25 commits into from
Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0f83009
Add MockDispatcher. Now enqueue is a special case of dispatch (via Qu…
BoD Dec 22, 2021
28a676f
Add ApolloMockDispatcher + Map and Queue implementations
BoD Dec 23, 2021
3fd55fb
Add TestNetworkTransport + Map and Queue dispatchers
BoD Dec 23, 2021
3c36f9b
Update apiDump
BoD Dec 23, 2021
9e22ad9
Fix recorded requests kept too early in JS MockServer
BoD Jan 4, 2022
a1240c4
Fix body being consumed when converting OkHttp's RecordedRequest to A…
BoD Jan 4, 2022
b318bd7
Remove experimental memory model, and put the freeze in Apple MockSer…
BoD Jan 4, 2022
9ed8c4e
Update apollo-mockserver/src/commonMain/kotlin/com/apollographql/apol…
BoD Jan 4, 2022
2d2717b
Reorganize tests
BoD Jan 4, 2022
9dda05e
Rename MockDispatcher -> MockServerHandler
BoD Jan 4, 2022
51336d5
Rename MockRecordedRequest -> MockRequest
BoD Jan 4, 2022
7e7e0e4
Rename TestNetworkTransportDispatcher -> TestNetworkTransportHandler
BoD Jan 4, 2022
9667216
A few more dispatcher -> handler renames
BoD Jan 4, 2022
7b9699d
A few more dispatcher -> handler renames
BoD Jan 4, 2022
313ef8b
Get rid of BaseMockServer which was not very useful
BoD Jan 4, 2022
efb213e
Remove the "manual" version of the TestNetworkTransport test
BoD Jan 4, 2022
f806cfa
Remove MockServerHandler.copy() and have a "freeze safe" version of Q…
BoD Jan 4, 2022
543f7b7
Remove ApolloMockServerHandler which was not really useful in the end
BoD Jan 4, 2022
760640f
Catch exceptions in handler and respond 500 instead of crashing
BoD Jan 5, 2022
e8621e8
Use TestNetworkTransport instead of MockWebServer in a few tests
BoD Jan 5, 2022
9b3b5fe
Mark TestNetworkTransport as @ApolloExperimental
BoD Jan 5, 2022
a32be34
Add locks around collections in TestNetworkTransportHandlers
BoD Jan 5, 2022
b598cc9
Add a custom TestNetworkTransportHandler test
BoD Jan 5, 2022
f85d0e6
Default to QueueTestNetworkTransportHandler in TestNetworkTransport
BoD Jan 6, 2022
4caac2f
Remove TestNetworkTransportHandler which was a bit over-engineered, i…
BoD Jan 10, 2022
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
2 changes: 1 addition & 1 deletion apollo-mockserver/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
POM_ARTIFACT_ID=apollo-mockserver
POM_NAME=Apollo Mockserver
POM_DESCRIPTION=Apollo Mockserver
POM_DESCRIPTION=Apollo Mockserver
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ import kotlin.native.concurrent.freeze
* call. Can be used to simulate slow connections.
*/
actual class MockServer(
private val acceptDelayMillis: Long
): MockServerInterface {
private val acceptDelayMillis: Long,
override val mockServerHandler: MockServerHandler = QueueMockServerHandler(),
) : MockServerInterface {

actual constructor(): this(0)
actual constructor(mockServerHandler: MockServerHandler) : this(0, mockServerHandler)

private val pthreadT: pthread_tVar
private val port: Int
Expand Down Expand Up @@ -70,7 +71,7 @@ actual class MockServer(

pthreadT = nativeHeap.alloc()

socket = Socket(socketFd, acceptDelayMillis)
socket = Socket(socketFd, acceptDelayMillis, mockServerHandler)

val stableRef = StableRef.create(socket!!.freeze())

Expand All @@ -79,23 +80,30 @@ actual class MockServer(

val ref = arg!!.asStableRef<Socket>()

ref.get().also {
ref.dispose()
}.run()
try {
ref.get().also {
ref.dispose()
}.run()
} catch (e: Throwable) {
println("MockServer socket thread crashed: $e")
e.printStackTrace()
}

null
}, stableRef.asCPointer())
}

override suspend fun url(): String {
return "http://localhost:$port"
return "http://localhost:$port/"
}

override fun enqueue(mockResponse: MockResponse) {
check(socket != null) {
"Cannot enqueue a response to a stopped MockServer"
}
socket!!.enqueue(mockResponse)
val queueMockServerHandler = mockServerHandler as? QueueMockServerHandler
?: error("Apollo: cannot call MockServer.enqueue() with a custom handler")
queueMockServerHandler.enqueue(mockResponse)
}

/**
Expand All @@ -116,7 +124,7 @@ actual class MockServer(
socket = null
}

override fun takeRequest(): MockRecordedRequest {
override fun takeRequest(): MockRequest {
check(socket != null) {
"Cannot take a request from a stopped MockServer"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.apollographql.apollo3.mockserver

import platform.Foundation.NSMutableArray
import kotlin.native.concurrent.freeze

internal actual class QueueMockServerHandler : MockServerHandler {
private val queue = NSMutableArray()

actual fun enqueue(response: MockResponse) {
queue.addObject(response.freeze())
}

actual override fun handle(request: MockRequest): MockResponse {
check(queue.count.toInt() > 0) {
"No more responses in queue"
}
val response = queue.objectAtIndex(0) as MockResponse
queue.removeObjectAtIndex(0)
return response
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ import kotlin.experimental.and
import kotlin.native.concurrent.AtomicInt
import kotlin.native.concurrent.freeze

class Socket(private val socketFd: Int, private val acceptDelayMillis: Long) {
class Socket(
private val socketFd: Int,
private val acceptDelayMillis: Long,
private val mockServerHandler: MockServerHandler,
) {
private val pipeFd = nativeHeap.allocArray<IntVar>(2)
private val running = AtomicInt(1)
private val lock = reentrantLock()
private val queuedResponses = NSMutableArray()
private val recordedRequests = NSMutableArray()

init {
Expand Down Expand Up @@ -81,7 +84,7 @@ class Socket(private val socketFd: Int, private val acceptDelayMillis: Long) {

val one = alloc<IntVar>()
one.value = 1
setsockopt(connectionFd, SOL_SOCKET, SO_NOSIGPIPE, one.ptr, 4);
setsockopt(connectionFd, SOL_SOCKET, SO_NOSIGPIPE, one.ptr, 4)

handleConnection(connectionFd)
close(connectionFd)
Expand Down Expand Up @@ -128,13 +131,11 @@ class Socket(private val socketFd: Int, private val acceptDelayMillis: Long) {

val mockResponse = synchronized(lock) {
recordedRequests.addObject(request.freeze())

check(queuedResponses.count.toInt() > 0) {
"no queued responses"
try {
mockServerHandler.handle(request)
} catch (e: Exception) {
MockResponse("MockServerHandler.handle() threw an exception: ${e.message}", 500)
}
queuedResponses.objectAtIndex(0).also {
queuedResponses.removeObjectAtIndex(0)
} as MockResponse
}

debug("Write response: ${mockResponse.statusCode}")
Expand Down Expand Up @@ -166,20 +167,14 @@ class Socket(private val socketFd: Int, private val acceptDelayMillis: Long) {
nativeHeap.free(pipeFd.rawValue)
}

fun enqueue(mockResponse: MockResponse) {
synchronized(lock) {
queuedResponses.addObject(mockResponse.freeze())
}
}

fun takeRequest(): MockRecordedRequest {
fun takeRequest(): MockRequest {
return synchronized(lock) {
check(recordedRequests.count.toInt() > 0) {
"no recorded request"
}
recordedRequests.objectAtIndex(0).also {
recordedRequests.removeObjectAtIndex(0)
} as MockRecordedRequest
} as MockRequest
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ interface MockServerInterface {
*/
suspend fun stop()

/**
* The mock server handler used to respond to requests.
*
* The default handler is a [QueueMockServerHandler], which serves a fixed sequence of responses from a queue (see [enqueue]).
*/
val mockServerHandler: MockServerHandler

/**
* Enqueue a response
*/
Expand All @@ -23,10 +30,8 @@ interface MockServerInterface {
/**
* Returns a request from the recorded requests or throws if no request has been received
*/
fun takeRequest(): MockRecordedRequest
fun takeRequest(): MockRequest
}


@ApolloExperimental
expect class MockServer() : MockServerInterface {
}
expect class MockServer(mockServerHandler: MockServerHandler = QueueMockServerHandler()) : MockServerInterface
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import okio.BufferedSource
import okio.ByteString
import okio.ByteString.Companion.encodeUtf8
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic

fun parseHeader(line: String): Pair<String, String> {
val index = line.indexOfFirst { it == ':' }
Expand All @@ -17,12 +16,12 @@ fun parseHeader(line: String): Pair<String, String> {
return line.substring(0, index).trim() to line.substring(index + 1, line.length).trim()
}

class MockRecordedRequest(
class MockRequest(
val method: String,
val path: String,
val version: String,
val headers: Map<String, String> = emptyMap(),
val body: ByteString = ByteString.EMPTY
val body: ByteString = ByteString.EMPTY,
)

fun writeResponse(sink: BufferedSink, mockResponse: MockResponse, version: String) {
Expand Down Expand Up @@ -51,11 +50,20 @@ class MockResponse(
constructor(
body: String,
statusCode: Int = 200,
headers: Map<String, String> = emptyMap()
headers: Map<String, String> = emptyMap(),
) : this(statusCode, body.encodeUtf8(), headers)
}

internal fun readRequest(source: BufferedSource): MockRecordedRequest? {
interface MockServerHandler {
/**
* Handles the given [MockRequest].
*
* This method is called from one or several background threads and must be thread-safe.
*/
fun handle(request: MockRequest): MockResponse
}

internal fun readRequest(source: BufferedSource): MockRequest? {
var line = source.readUtf8Line()
if (line == null) {
// the connection was closed
Expand Down Expand Up @@ -90,7 +98,7 @@ internal fun readRequest(source: BufferedSource): MockRecordedRequest? {
source.read(buffer, contentLength)
}

return MockRecordedRequest(
return MockRequest(
method = method,
path = path,
version = version,
Expand All @@ -112,4 +120,4 @@ fun parseRequestLine(line: String): Triple<String, String, String> {
}

return Triple(method, match.groupValues[2], match.groupValues[3])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.apollographql.apollo3.mockserver

internal expect class QueueMockServerHandler() : MockServerHandler {
fun enqueue(response: MockResponse)

override fun handle(request: MockRequest): MockResponse
}

internal class CommonQueueMockServerHandler : MockServerHandler {
private val queue = ArrayDeque<MockResponse>()

fun enqueue(response: MockResponse) {
queue.add(response)
}

override fun handle(request: MockRequest): MockResponse {
return queue.removeFirstOrNull() ?: error("No more responses in queue")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.apollographql.apollo3.mockserver.test

import com.apollographql.apollo3.annotations.ApolloExperimental
import com.apollographql.apollo3.api.http.HttpMethod
import com.apollographql.apollo3.api.http.HttpRequest
import com.apollographql.apollo3.mockserver.MockRequest
import com.apollographql.apollo3.mockserver.MockResponse
import com.apollographql.apollo3.mockserver.MockServer
import com.apollographql.apollo3.mockserver.MockServerHandler
import com.apollographql.apollo3.network.http.DefaultHttpEngine
import com.apollographql.apollo3.testing.runTest
import kotlin.test.Test

@OptIn(ApolloExperimental::class)
class CustomHandlerTest {
private lateinit var mockServer: MockServer

private suspend fun tearDown() {
mockServer.stop()
}

@Test
fun customHandler() = runTest(after = { tearDown() }) {
val mockResponse0 = MockResponse(
body = "Hello, World! 000",
statusCode = 404,
headers = mapOf("Content-Type" to "text/plain"),
)
val mockResponse1 = MockResponse(
body = "Hello, World! 001",
statusCode = 200,
headers = mapOf("X-Test" to "true"),
)

val mockServerHandler = object : MockServerHandler {
override fun handle(request: MockRequest): MockResponse {
return when (request.path) {
"/0" -> mockResponse0
"/1" -> mockResponse1
else -> error("Unexpected path: ${request.path}")
}
}
}

mockServer = MockServer(mockServerHandler)

val engine = DefaultHttpEngine()

var httpResponse = engine.execute(HttpRequest.Builder(HttpMethod.Get, mockServer.url() + "1").build())
assertMockResponse(mockResponse1, httpResponse)

httpResponse = engine.execute(HttpRequest.Builder(HttpMethod.Get, mockServer.url() + "0").build())
assertMockResponse(mockResponse0, httpResponse)

httpResponse = engine.execute(HttpRequest.Builder(HttpMethod.Get, mockServer.url() + "1").build())
assertMockResponse(mockResponse1, httpResponse)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.apollographql.apollo3.mockserver.test

import com.apollographql.apollo3.annotations.ApolloExperimental
import com.apollographql.apollo3.api.http.HttpMethod
import com.apollographql.apollo3.api.http.HttpRequest
import com.apollographql.apollo3.mockserver.MockResponse
import com.apollographql.apollo3.mockserver.MockServer
import com.apollographql.apollo3.network.http.DefaultHttpEngine
import com.apollographql.apollo3.testing.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@OptIn(ApolloExperimental::class)
class EnqueueTest {
private lateinit var mockServer: MockServer

private fun setUp() {
mockServer = MockServer()
}

private suspend fun tearDown() {
mockServer.stop()
}

@Test
fun enqueue() = runTest(before = { setUp() }, after = { tearDown() }) {
val mockResponses = listOf(
MockResponse(
body = "Hello, World! 000",
statusCode = 404,
headers = mapOf("Content-Type" to "text/plain"),
),
MockResponse(
body = "Hello, World! 001",
statusCode = 200,
headers = mapOf("X-Test" to "true"),
),
)
for (mockResponse in mockResponses) {
mockServer.enqueue(mockResponse)
}

val engine = DefaultHttpEngine()
for (mockResponse in mockResponses) {
val httpResponse = engine.execute(HttpRequest.Builder(HttpMethod.Get, mockServer.url()).build())
assertMockResponse(mockResponse, httpResponse)
}
}

@Test
fun status500WhenNothingWasEnqueued() = runTest(before = { setUp() }, after = { tearDown() }) {
val engine = DefaultHttpEngine()
val httpResponse = engine.execute(HttpRequest.Builder(HttpMethod.Get, mockServer.url()).build())
assertEquals(500, httpResponse.statusCode)
assertTrue(httpResponse.body!!.readUtf8().contains("No more responses in queue"))
}
}
Loading