-
Notifications
You must be signed in to change notification settings - Fork 1k
/
TestHttpClientEngine.kt
127 lines (107 loc) · 4.15 KB
/
TestHttpClientEngine.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/*
* Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/
package io.ktor.server.testing.client
import io.ktor.client.call.*
import io.ktor.client.engine.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.testing.*
import io.ktor.server.testing.internal.*
import io.ktor.util.*
import io.ktor.util.date.*
import io.ktor.utils.io.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
import kotlin.coroutines.*
internal expect class TestHttpClientEngineBridge(engine: TestHttpClientEngine, app: TestApplicationEngine) {
val supportedCapabilities: Set<HttpClientEngineCapability<*>>
suspend fun runWebSocketRequest(
url: String,
headers: Headers,
content: OutgoingContent,
coroutineContext: CoroutineContext
): Pair<TestApplicationCall, WebSocketSession>
}
public class TestHttpClientEngine(override val config: TestHttpClientConfig) : HttpClientEngineBase("ktor-test") {
private val app: TestApplicationEngine = config.app
private val bridge = TestHttpClientEngineBridge(this, app)
override val supportedCapabilities = bridge.supportedCapabilities
override val dispatcher = Dispatchers.IOBridge
private val clientJob: CompletableJob = Job(app.coroutineContext[Job])
override val coroutineContext: CoroutineContext = dispatcher + clientJob
@OptIn(InternalAPI::class)
override suspend fun execute(data: HttpRequestData): HttpResponseData {
app.start()
if (data.isUpgradeRequest()) {
val (testServerCall, session) = with(data) {
bridge.runWebSocketRequest(url.fullPath, headers, body, callContext())
}
return with(testServerCall.response) {
httpResponseData(session)
}
}
val testServerCall = with(data) {
runRequest(method, url, headers, body, url.protocol)
}
return with(testServerCall.response) {
httpResponseData(ByteReadChannel(byteContent ?: byteArrayOf()))
}
}
private suspend fun runRequest(
method: HttpMethod,
url: Url,
headers: Headers,
content: OutgoingContent,
protocol: URLProtocol
): TestApplicationCall {
return app.handleRequestNonBlocking {
this.uri = url.fullPath
this.port = url.port
this.method = method
appendRequestHeaders(headers, content)
this.protocol = protocol.name
if (content !is OutgoingContent.NoContent) {
bodyChannel = content.toByteReadChannel()
}
}
}
@OptIn(InternalAPI::class)
private suspend fun TestApplicationResponse.httpResponseData(body: Any) = HttpResponseData(
status() ?: HttpStatusCode.NotFound,
GMTDate(),
headers.allValues().takeUnless { it.isEmpty() } ?: Headers
.build { append(HttpHeaders.ContentLength, "0") },
HttpProtocolVersion.HTTP_1_1,
body,
callContext()
)
@OptIn(InternalAPI::class)
internal fun TestApplicationRequest.appendRequestHeaders(
headers: Headers,
content: OutgoingContent
) {
mergeHeaders(headers, content) { name, value ->
addHeader(name, value)
}
}
override fun close() {
clientJob.complete()
}
companion object : HttpClientEngineFactory<TestHttpClientConfig> {
override fun create(block: TestHttpClientConfig.() -> Unit): HttpClientEngine {
val config = TestHttpClientConfig().apply(block)
return TestHttpClientEngine(config)
}
}
private fun OutgoingContent.toByteReadChannel(): ByteReadChannel = when (this) {
is OutgoingContent.NoContent -> ByteReadChannel.Empty
is OutgoingContent.ByteArrayContent -> ByteReadChannel(bytes())
is OutgoingContent.ReadChannelContent -> readFrom()
is OutgoingContent.WriteChannelContent -> writer(coroutineContext) {
writeTo(channel)
}.channel
is OutgoingContent.ProtocolUpgrade -> throw UnsupportedContentTypeException(this)
}
}