新增本地 mock 接口返回数据
This commit is contained in:
@ -3,6 +3,8 @@ package com.gh.gamecenter;
|
||||
import android.app.Application;
|
||||
|
||||
|
||||
import com.gh.gamecenter.mock.MockConfig;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
|
||||
@ -25,6 +27,7 @@ public class Injection {
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
builder.addNetworkInterceptor(interceptor);
|
||||
MockConfig.addBuilderMock(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package com.gh.gamecenter.mock
|
||||
|
||||
import com.gh.gamecenter.mock.mockinizer.mockinize
|
||||
import okhttp3.OkHttpClient
|
||||
object MockConfig {
|
||||
|
||||
private const val MOCK_SWITCH_IS_ON: Boolean = false
|
||||
|
||||
@JvmStatic
|
||||
fun addBuilderMock(builder: OkHttpClient.Builder) {
|
||||
if (MOCK_SWITCH_IS_ON)
|
||||
builder.mockinize(mocks)
|
||||
}
|
||||
}
|
||||
23
module_common/src/debug/java/com/gh/gamecenter/mock/Mocks.kt
Normal file
23
module_common/src/debug/java/com/gh/gamecenter/mock/Mocks.kt
Normal file
@ -0,0 +1,23 @@
|
||||
package com.gh.gamecenter.mock
|
||||
|
||||
import com.gh.gamecenter.common.HaloApp
|
||||
import com.gh.gamecenter.mock.mockinizer.RequestFilter
|
||||
import com.gh.gamecenter.mock.mockwebserver3.MockResponse
|
||||
|
||||
|
||||
private fun getJsonByAssets(fileName: String): String{
|
||||
return HaloApp.getInstance().resources.assets.open("assistant-android-mock/apiMock/${fileName}").bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
}
|
||||
|
||||
val mocks: Map<RequestFilter, MockResponse> = mapOf(
|
||||
|
||||
RequestFilter("/v5d5d0/home/union") to MockResponse().apply {
|
||||
setResponseCode(200)
|
||||
setBody(
|
||||
getJsonByAssets("home_union.json")
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
@ -0,0 +1,13 @@
|
||||
package com.gh.gamecenter.mock.mockinizer
|
||||
|
||||
import android.util.Log
|
||||
|
||||
object DebugLogger : Logger {
|
||||
override fun d(log: String) {
|
||||
Log.d("Mockinizer", log)
|
||||
}
|
||||
}
|
||||
|
||||
interface Logger {
|
||||
fun d(log: String)
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.gh.gamecenter.mock.mockinizer
|
||||
|
||||
import com.gh.gamecenter.mock.mockwebserver3.MockWebServer
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.tls.HandshakeCertificates
|
||||
import okhttp3.tls.HeldCertificate
|
||||
|
||||
|
||||
//原库是固定了一个port = 34567端口 但是固定一个端口的话 会产生崩溃 项目有多个apiService的创建 这边也会调用多次 导致端口被占用
|
||||
internal fun MockWebServer.configure(port: Int = ((0..9999).random() + 30000)): MockWebServer {
|
||||
|
||||
GlobalScope.launch {
|
||||
start(port)
|
||||
}
|
||||
|
||||
val localhostCertificate = HeldCertificate.Builder()
|
||||
.addSubjectAlternativeName("127.0.0.1")
|
||||
.build()
|
||||
|
||||
val serverCertificates = HandshakeCertificates.Builder()
|
||||
.heldCertificate(localhostCertificate)
|
||||
.build()
|
||||
|
||||
useHttps(serverCertificates.sslSocketFactory(), false)
|
||||
return this
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package com.gh.gamecenter.mock.mockinizer
|
||||
|
||||
import com.gh.gamecenter.mock.mockwebserver3.MockResponse
|
||||
import com.gh.gamecenter.mock.mockwebserver3.MockWebServer
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
|
||||
|
||||
class MockinizerInterceptor(
|
||||
private val mocks: Map<RequestFilter, MockResponse> = emptyMap(),
|
||||
private val mockServer: MockWebServer,
|
||||
private val log: Logger = DebugLogger
|
||||
) : Interceptor {
|
||||
|
||||
|
||||
private fun matchMockForFilterUrl(request: Request): MockResponse? {
|
||||
val urlPath = request.url().uri().path
|
||||
for ((requestFilter, mockResponse) in mocks) {
|
||||
if (requestFilter.path == urlPath) {
|
||||
return mockResponse
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
|
||||
fun findMockResponse(request: Request): MockResponse? {
|
||||
matchMockForFilterUrl(request)
|
||||
return with(RequestFilter.from(request, log)) {
|
||||
val foundMockResponse = matchMockForFilterUrl(request)
|
||||
?: mocks[this]
|
||||
?: mocks[this.copy(body = null)]
|
||||
?: mocks[this.copy(headers = null)]
|
||||
?: mocks[this.copy(body = null, headers = null)]
|
||||
|
||||
if (foundMockResponse == null) {
|
||||
log.d("No mocks found for $request")
|
||||
} else {
|
||||
log.d(
|
||||
"Found Mock response: $foundMockResponse " +
|
||||
"for request: $request"
|
||||
)
|
||||
}
|
||||
|
||||
foundMockResponse
|
||||
}
|
||||
}
|
||||
|
||||
fun Interceptor.Chain.findServer(): HttpUrl =
|
||||
when (findMockResponse(request())) {
|
||||
is MockResponse -> {
|
||||
request().url().newBuilder()
|
||||
.host(mockServer.hostName)
|
||||
.port(mockServer.port)
|
||||
// .scheme("http") //TODO: make http - https configurable
|
||||
.build()
|
||||
}
|
||||
else ->
|
||||
request().url()
|
||||
}
|
||||
|
||||
return with(chain) {
|
||||
proceed(
|
||||
request().newBuilder()
|
||||
.url(findServer())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,153 @@
|
||||
package com.gh.gamecenter.mock.mockinizer
|
||||
|
||||
import com.gh.gamecenter.mock.mockwebserver3.Dispatcher
|
||||
import com.gh.gamecenter.mock.mockwebserver3.MockResponse
|
||||
import com.gh.gamecenter.mock.mockwebserver3.MockWebServer
|
||||
import com.gh.gamecenter.mock.mockwebserver3.RecordedRequest
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.*
|
||||
|
||||
|
||||
/**
|
||||
* The main function that wires up the [MockWebServer] with [OkHttpClient]. Generally only the
|
||||
* mocks map needs to be defined. The default values for the other params should be fine for most
|
||||
* projects.
|
||||
*
|
||||
* @param mocks Map of RequestFilter / MockResponse Entries to define requests that
|
||||
* should be directed to the mock server instead of the real one. default value is an empty map.
|
||||
* @param trustManagers Array of TrustManager to be used for https connections with mock server
|
||||
* default value is an all trusting manager
|
||||
* @param socketFactory SSLSocketFactory to be used for RetroFit https connections
|
||||
* default value is using the previously defined trustManagers
|
||||
* @param hostnameVerifier HostNameVerifier the interface to be used to verify hostnames.
|
||||
* default value is an all verifying verifier
|
||||
*
|
||||
* @return OkHttpClient.Builder for chaining
|
||||
*/
|
||||
fun OkHttpClient.Builder.mockinize(
|
||||
mocks: Map<RequestFilter, MockResponse> = mapOf(),
|
||||
mockWebServer: MockWebServer = MockWebServer().configure(),
|
||||
trustManagers: Array<TrustManager> = getAllTrustingManagers(),
|
||||
socketFactory: SSLSocketFactory = getSslSocketFactory(trustManagers),
|
||||
hostnameVerifier: HostnameVerifier = HostnameVerifier { _, _ -> true },
|
||||
log: Logger = DebugLogger
|
||||
): OkHttpClient.Builder {
|
||||
|
||||
addInterceptor(MockinizerInterceptor(mocks, mockWebServer))
|
||||
.sslSocketFactory(socketFactory, trustManagers[0] as X509TrustManager)
|
||||
.hostnameVerifier(hostnameVerifier)
|
||||
Mockinizer.init(mockWebServer, mocks)
|
||||
log.d("Mockinized $this with mocks: $mocks and MockWebServer $mockWebServer")
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getSslSocketFactory(trustManagers: Array<TrustManager>): SSLSocketFactory =
|
||||
SSLContext.getInstance("SSL").apply {
|
||||
init(null, trustManagers, java.security.SecureRandom())
|
||||
}.socketFactory
|
||||
|
||||
private fun getAllTrustingManagers(): Array<TrustManager> = arrayOf(
|
||||
object : X509TrustManager {
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String
|
||||
) {
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>,
|
||||
authType: String
|
||||
) {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
internal class MockDispatcher(private val mocks: Map<RequestFilter, MockResponse>) : Dispatcher() {
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
return with(RequestFilter.from(request)) {
|
||||
mocks[this]
|
||||
?: mocks[copy(body = null)]
|
||||
?: mocks[copy(headers = request.headers.withClearedOkhttpHeaders())]
|
||||
?: mocks[copy(headers = null)]
|
||||
?: mocks[copy(query = null)]
|
||||
?: mocks[copy(body = null, headers = request.headers.withClearedOkhttpHeaders())]
|
||||
?: mocks[copy(body = null, headers = null)]
|
||||
?: mocks[copy(body = null, query = null)]
|
||||
?: mocks[copy(headers = null, query = null)]
|
||||
?: mocks[copy(headers = request.headers.withClearedOkhttpHeaders(), query = null)]
|
||||
?: mocks[copy(body = null, headers = null, query = null)]
|
||||
?: mocks[copy(body = null, headers = request.headers.withClearedOkhttpHeaders(), query = null)]
|
||||
?: MockResponse()
|
||||
.setResponseCode(404)
|
||||
.setBody("""{
|
||||
|"error":"Mockinizer could not dispatch response for $request",
|
||||
|"requestFilter":"$this""
|
||||
|}""".trimMargin())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes headers that OkHttp would add to RecordedRequest
|
||||
*/
|
||||
private fun Headers.withClearedOkhttpHeaders() =
|
||||
if (
|
||||
get(":authority")?.startsWith("localhost:") == true &&
|
||||
get(":scheme")?.matches("https?".toRegex()) == true &&
|
||||
get("accept-encoding") == "gzip" &&
|
||||
get("user-agent")?.startsWith("okhttp/") == true
|
||||
) {
|
||||
newBuilder()
|
||||
.removeAll(":authority")
|
||||
.removeAll(":scheme")
|
||||
.removeAll("accept-encoding")
|
||||
.removeAll("user-agent")
|
||||
.build()
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
object Mockinizer {
|
||||
|
||||
internal var mockWebServer: MockWebServer? = null
|
||||
|
||||
internal fun init(
|
||||
mockWebServer: MockWebServer,
|
||||
mocks: Map<RequestFilter, MockResponse>
|
||||
) {
|
||||
|
||||
mocks.entries.forEach { (requestFilter, mockResponse) ->
|
||||
mockResponse.setHeader(
|
||||
"Mockinizer",
|
||||
" <-- Real request ${requestFilter.path} is now mocked to $mockResponse"
|
||||
)
|
||||
mockResponse.setHeader(
|
||||
"server",
|
||||
"Mockinizer by Thomas Fuchs-Martin"
|
||||
)
|
||||
}
|
||||
|
||||
mockWebServer.dispatcher = MockDispatcher(mocks)
|
||||
|
||||
Mockinizer.mockWebServer = mockWebServer
|
||||
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun start(port: Int = 34567) {
|
||||
mockWebServer?.start(port)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun shutDown() {
|
||||
mockWebServer?.shutdown()
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package com.gh.gamecenter.mock.mockinizer
|
||||
|
||||
import com.gh.gamecenter.mock.mockwebserver3.RecordedRequest
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
|
||||
/**
|
||||
* This class is to define the requests that should get filtered and served by the mock server.
|
||||
* Setting a parameter to null means that any values for that parameter should be filtered.
|
||||
* @param path the path part of the request url. The default is null
|
||||
* @param query the query part of the request url. The default is null
|
||||
* @param method the method of the request. The default is GET
|
||||
* @param body the request body. Cannot be used together with GET requests. The default is null
|
||||
* @param headers the http headers to filter. The default is null headers
|
||||
*/
|
||||
data class RequestFilter(
|
||||
val path: String? = null,
|
||||
val query: String? = null,
|
||||
val method: Method = Method.GET,
|
||||
val body: String? = null,
|
||||
val headers: Headers? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun from(request: Request, log: Logger = DebugLogger) =
|
||||
RequestFilter(
|
||||
path = request.url().encodedPath(),
|
||||
query = request.url().encodedQuery(),
|
||||
method = getMethodOrDefault(request.method()),
|
||||
body = request.body()?.asString(),
|
||||
headers = request.headers()
|
||||
).also {
|
||||
log.d(
|
||||
"Created RequestFilter $it \n" +
|
||||
" for request: $request"
|
||||
)
|
||||
}
|
||||
|
||||
fun from(request: RecordedRequest, log: Logger = DebugLogger) =
|
||||
RequestFilter(
|
||||
path = request.requestUrl?.encodedPath(),
|
||||
query = request.requestUrl?.encodedQuery(),
|
||||
method = getMethodOrDefault(request.method),
|
||||
body = request.body.clone().readUtf8(),
|
||||
headers = request.headers
|
||||
).also {
|
||||
log.d(
|
||||
"Created RequestFilter $it \n" +
|
||||
" for recorded request: $request"
|
||||
)
|
||||
}
|
||||
|
||||
private fun getMethodOrDefault(method: String?) =
|
||||
try {
|
||||
Method.valueOf(method.orEmpty())
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Method.GET
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Method {
|
||||
GET, POST, PUT, PATCH, DELETE;
|
||||
}
|
||||
|
||||
fun RequestFilter.url(): String = when (query.isNullOrBlank()) {
|
||||
true -> path.orEmpty()
|
||||
false -> path.orEmpty().plus("?").plus(query)
|
||||
}
|
||||
|
||||
fun RequestBody.asString(): String {
|
||||
val buffer = Buffer()
|
||||
writeTo(buffer)
|
||||
return buffer.readUtf8()
|
||||
}
|
||||
|
||||
fun RecordedRequest.asString(): String {
|
||||
return "$path - $method - ${body.clone().readUtf8()} - $headers"
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.gh.gamecenter.mock.mockwebserver3
|
||||
|
||||
|
||||
/** Handler for mock server requests. */
|
||||
abstract class Dispatcher {
|
||||
/**
|
||||
* Returns a response to satisfy `request`. This method may block (for instance, to wait on
|
||||
* a CountdownLatch).
|
||||
*/
|
||||
@Throws(InterruptedException::class)
|
||||
abstract fun dispatch(request: RecordedRequest): MockResponse
|
||||
|
||||
/**
|
||||
* Returns an early guess of the next response, used for policy on how an incoming request should
|
||||
* be received. The default implementation returns an empty response. Mischievous implementations
|
||||
* can return other values to test HTTP edge cases, such as unhappy socket policies or throttled
|
||||
* request bodies.
|
||||
*/
|
||||
open fun peek(): MockResponse {
|
||||
return MockResponse().apply { this.socketPolicy = SocketPolicy.KEEP_OPEN }
|
||||
}
|
||||
|
||||
/**
|
||||
* Release any resources held by this dispatcher. Any requests that are currently being dispatched
|
||||
* should return immediately. Responses returned after shutdown will not be transmitted: their
|
||||
* socket connections have already been closed.
|
||||
*/
|
||||
open fun shutdown() {}
|
||||
}
|
||||
@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.gh.gamecenter.mock.mockwebserver3
|
||||
|
||||
import com.gh.gamecenter.mock.mockwebserver3.internal.duplex.DuplexResponseBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.WebSocketListener
|
||||
import okhttp3.internal.http2.Settings
|
||||
import okio.Buffer
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** A scripted response to be replayed by the mock web server. */
|
||||
class MockResponse : Cloneable {
|
||||
/** Returns the HTTP response line, such as "HTTP/1.1 200 OK". */
|
||||
@set:JvmName("status")
|
||||
var status: String = ""
|
||||
|
||||
private var headersBuilder = Headers.Builder()
|
||||
private var trailersBuilder = Headers.Builder()
|
||||
|
||||
/** The HTTP headers, such as "Content-Length: 0". */
|
||||
@set:JvmName("headers")
|
||||
var headers: Headers
|
||||
get() = headersBuilder.build()
|
||||
set(value) {
|
||||
this.headersBuilder = value.newBuilder()
|
||||
}
|
||||
|
||||
@set:JvmName("trailers")
|
||||
var trailers: Headers
|
||||
get() = trailersBuilder.build()
|
||||
set(value) {
|
||||
this.trailersBuilder = value.newBuilder()
|
||||
}
|
||||
|
||||
private var body: Buffer? = null
|
||||
|
||||
var throttleBytesPerPeriod = Long.MAX_VALUE
|
||||
private set
|
||||
private var throttlePeriodAmount = 1L
|
||||
private var throttlePeriodUnit = TimeUnit.SECONDS
|
||||
|
||||
@set:JvmName("socketPolicy")
|
||||
var socketPolicy = SocketPolicy.KEEP_OPEN
|
||||
|
||||
/**
|
||||
* Sets the [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) to be
|
||||
* returned when resetting the stream.
|
||||
* This is only valid with [SocketPolicy.RESET_STREAM_AT_START].
|
||||
*/
|
||||
@set:JvmName("http2ErrorCode")
|
||||
var http2ErrorCode = -1
|
||||
|
||||
private var bodyDelayAmount = 0L
|
||||
private var bodyDelayUnit = TimeUnit.MILLISECONDS
|
||||
|
||||
private var headersDelayAmount = 0L
|
||||
private var headersDelayUnit = TimeUnit.MILLISECONDS
|
||||
|
||||
private var promises = mutableListOf<PushPromise>()
|
||||
var settings: Settings = Settings()
|
||||
private set
|
||||
var webSocketListener: WebSocketListener? = null
|
||||
private set
|
||||
var duplexResponseBody: DuplexResponseBody? = null
|
||||
private set
|
||||
val isDuplex: Boolean
|
||||
get() = duplexResponseBody != null
|
||||
|
||||
/** Returns the streams the server will push with this response. */
|
||||
val pushPromises: List<PushPromise>
|
||||
get() = promises
|
||||
|
||||
/** Creates a new mock response with an empty body. */
|
||||
init {
|
||||
setResponseCode(200)
|
||||
setHeader("Content-Length", 0L)
|
||||
}
|
||||
|
||||
public override fun clone(): MockResponse {
|
||||
val result = super.clone() as MockResponse
|
||||
result.headersBuilder = headersBuilder.build().newBuilder()
|
||||
result.promises = promises.toMutableList()
|
||||
return result
|
||||
}
|
||||
|
||||
@JvmName("-deprecated_getStatus")
|
||||
@Deprecated(
|
||||
message = "moved to var",
|
||||
replaceWith = ReplaceWith(expression = "status"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun getStatus(): String = status
|
||||
|
||||
@Deprecated(
|
||||
message = "moved to var. Replace setStatus(...) with status(...) to fix Java",
|
||||
replaceWith = ReplaceWith(expression = "apply { this.status = status }"),
|
||||
level = DeprecationLevel.WARNING)
|
||||
fun setStatus(status: String) = apply {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
fun setResponseCode(code: Int): MockResponse {
|
||||
val reason = when (code) {
|
||||
in 100..199 -> "Informational"
|
||||
in 200..299 -> "OK"
|
||||
in 300..399 -> "Redirection"
|
||||
in 400..499 -> "Client Error"
|
||||
in 500..599 -> "Server Error"
|
||||
else -> "Mock Response"
|
||||
}
|
||||
return apply { status = "HTTP/1.1 $code $reason" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all HTTP headers including any "Content-Length" and "Transfer-encoding" headers that
|
||||
* were added by default.
|
||||
*/
|
||||
fun clearHeaders() = apply {
|
||||
headersBuilder = Headers.Builder()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds [header] as an HTTP header. For well-formed HTTP [header] should contain a
|
||||
* name followed by a colon and a value.
|
||||
*/
|
||||
fun addHeader(header: String) = apply {
|
||||
headersBuilder.add(header)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new header with the name and value. This may be used to add multiple headers with the
|
||||
* same name.
|
||||
*/
|
||||
fun addHeader(name: String, value: Any) = apply {
|
||||
headersBuilder.add(name, value.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new header with the name and value. This may be used to add multiple headers with the
|
||||
* same name. Unlike [addHeader] this does not validate the name and
|
||||
* value.
|
||||
*/
|
||||
fun addHeaderLenient(name: String, value: Any) = apply {
|
||||
headersBuilder.add(name, value.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all headers named [name], then adds a new header with the name and value.
|
||||
*/
|
||||
fun setHeader(name: String, value: Any) = apply {
|
||||
removeHeader(name)
|
||||
addHeader(name, value)
|
||||
}
|
||||
|
||||
/** Removes all headers named [name]. */
|
||||
fun removeHeader(name: String) = apply {
|
||||
headersBuilder.removeAll(name)
|
||||
}
|
||||
|
||||
/** Returns a copy of the raw HTTP payload. */
|
||||
fun getBody(): Buffer? = body?.clone()
|
||||
|
||||
fun setBody(body: Buffer) = apply {
|
||||
setHeader("Content-Length", body.size)
|
||||
this.body = body.clone() // Defensive copy.
|
||||
}
|
||||
|
||||
/** Sets the response body to the UTF-8 encoded bytes of [body]. */
|
||||
fun setBody(body: String): MockResponse = setBody(Buffer().writeUtf8(body))
|
||||
|
||||
fun setBody(duplexResponseBody: DuplexResponseBody) = apply {
|
||||
this.duplexResponseBody = duplexResponseBody
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the response body to [body], chunked every [maxChunkSize] bytes.
|
||||
*/
|
||||
fun setChunkedBody(body: Buffer, maxChunkSize: Int) = apply {
|
||||
removeHeader("Content-Length")
|
||||
headersBuilder.add(CHUNKED_BODY_HEADER)
|
||||
|
||||
val bytesOut = Buffer()
|
||||
while (!body.exhausted()) {
|
||||
val chunkSize = minOf(body.size, maxChunkSize.toLong())
|
||||
bytesOut.writeHexadecimalUnsignedLong(chunkSize)
|
||||
bytesOut.writeUtf8("\r\n")
|
||||
bytesOut.write(body, chunkSize)
|
||||
bytesOut.writeUtf8("\r\n")
|
||||
}
|
||||
bytesOut.writeUtf8("0\r\n") // Last chunk. Trailers follow!
|
||||
this.body = bytesOut
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the response body to the UTF-8 encoded bytes of [body],
|
||||
* chunked every [maxChunkSize] bytes.
|
||||
*/
|
||||
fun setChunkedBody(body: String, maxChunkSize: Int): MockResponse =
|
||||
setChunkedBody(Buffer().writeUtf8(body), maxChunkSize)
|
||||
|
||||
@JvmName("-deprecated_getHeaders")
|
||||
@Deprecated(
|
||||
message = "moved to var",
|
||||
replaceWith = ReplaceWith(expression = "headers"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun getHeaders(): Headers = headers
|
||||
|
||||
@Deprecated(
|
||||
message = "moved to var. Replace setHeaders(...) with headers(...) to fix Java",
|
||||
replaceWith = ReplaceWith(expression = "apply { this.headers = headers }"),
|
||||
level = DeprecationLevel.WARNING)
|
||||
fun setHeaders(headers: Headers) = apply { this.headers = headers }
|
||||
|
||||
@JvmName("-deprecated_getTrailers")
|
||||
@Deprecated(
|
||||
message = "moved to var",
|
||||
replaceWith = ReplaceWith(expression = "trailers"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun getTrailers(): Headers = trailers
|
||||
|
||||
@Deprecated(
|
||||
message = "moved to var. Replace setTrailers(...) with trailers(...) to fix Java",
|
||||
replaceWith = ReplaceWith(expression = "apply { this.trailers = trailers }"),
|
||||
level = DeprecationLevel.WARNING)
|
||||
fun setTrailers(trailers: Headers) = apply { this.trailers = trailers }
|
||||
|
||||
@JvmName("-deprecated_getSocketPolicy")
|
||||
@Deprecated(
|
||||
message = "moved to var",
|
||||
replaceWith = ReplaceWith(expression = "socketPolicy"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun getSocketPolicy() = socketPolicy
|
||||
|
||||
@Deprecated(
|
||||
message = "moved to var. Replace setSocketPolicy(...) with socketPolicy(...) to fix Java",
|
||||
replaceWith = ReplaceWith(expression = "apply { this.socketPolicy = socketPolicy }"),
|
||||
level = DeprecationLevel.WARNING)
|
||||
fun setSocketPolicy(socketPolicy: SocketPolicy) = apply {
|
||||
this.socketPolicy = socketPolicy
|
||||
}
|
||||
|
||||
@JvmName("-deprecated_getHttp2ErrorCode")
|
||||
@Deprecated(
|
||||
message = "moved to var",
|
||||
replaceWith = ReplaceWith(expression = "http2ErrorCode"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun getHttp2ErrorCode() = http2ErrorCode
|
||||
|
||||
@Deprecated(
|
||||
message = "moved to var. Replace setHttp2ErrorCode(...) with http2ErrorCode(...) to fix Java",
|
||||
replaceWith = ReplaceWith(expression = "apply { this.http2ErrorCode = http2ErrorCode }"),
|
||||
level = DeprecationLevel.WARNING)
|
||||
fun setHttp2ErrorCode(http2ErrorCode: Int) = apply {
|
||||
this.http2ErrorCode = http2ErrorCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttles the request reader and response writer to sleep for the given period after each
|
||||
* series of [bytesPerPeriod] bytes are transferred. Use this to simulate network behavior.
|
||||
*/
|
||||
fun throttleBody(bytesPerPeriod: Long, period: Long, unit: TimeUnit) = apply {
|
||||
throttleBytesPerPeriod = bytesPerPeriod
|
||||
throttlePeriodAmount = period
|
||||
throttlePeriodUnit = unit
|
||||
}
|
||||
|
||||
fun getThrottlePeriod(unit: TimeUnit): Long =
|
||||
unit.convert(throttlePeriodAmount, throttlePeriodUnit)
|
||||
|
||||
/**
|
||||
* Set the delayed time of the response body to [delay]. This applies to the response body
|
||||
* only; response headers are not affected.
|
||||
*/
|
||||
fun setBodyDelay(delay: Long, unit: TimeUnit) = apply {
|
||||
bodyDelayAmount = delay
|
||||
bodyDelayUnit = unit
|
||||
}
|
||||
|
||||
fun getBodyDelay(unit: TimeUnit): Long =
|
||||
unit.convert(bodyDelayAmount, bodyDelayUnit)
|
||||
|
||||
fun setHeadersDelay(delay: Long, unit: TimeUnit) = apply {
|
||||
headersDelayAmount = delay
|
||||
headersDelayUnit = unit
|
||||
}
|
||||
|
||||
fun getHeadersDelay(unit: TimeUnit): Long =
|
||||
unit.convert(headersDelayAmount, headersDelayUnit)
|
||||
|
||||
/**
|
||||
* When [protocols][MockWebServer.protocols] include [HTTP_2][okhttp3.Protocol], this attaches a
|
||||
* pushed stream to this response.
|
||||
*/
|
||||
fun withPush(promise: PushPromise) = apply {
|
||||
promises.add(promise)
|
||||
}
|
||||
|
||||
/**
|
||||
* When [protocols][MockWebServer.protocols] include [HTTP_2][okhttp3.Protocol], this pushes
|
||||
* [settings] before writing the response.
|
||||
*/
|
||||
fun withSettings(settings: Settings) = apply {
|
||||
this.settings = settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to perform a web socket upgrade on the connection.
|
||||
* This will overwrite any previously set status or body.
|
||||
*/
|
||||
fun withWebSocketUpgrade(listener: WebSocketListener) = apply {
|
||||
status = "HTTP/1.1 101 Switching Protocols"
|
||||
setHeader("Connection", "Upgrade")
|
||||
setHeader("Upgrade", "websocket")
|
||||
body = null
|
||||
webSocketListener = listener
|
||||
}
|
||||
|
||||
override fun toString() = status
|
||||
|
||||
companion object {
|
||||
private const val CHUNKED_BODY_HEADER = "Transfer-encoding: chunked"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.gh.gamecenter.mock.mockwebserver3
|
||||
|
||||
|
||||
import okhttp3.Headers
|
||||
/** An HTTP request initiated by the server. */
|
||||
class PushPromise(
|
||||
@get:JvmName("method") val method: String,
|
||||
@get:JvmName("path") val path: String,
|
||||
@get:JvmName("headers") val headers: Headers,
|
||||
@get:JvmName("response") val response: MockResponse
|
||||
) {
|
||||
|
||||
@JvmName("-deprecated_method")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "method"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun method() = method
|
||||
|
||||
@JvmName("-deprecated_path")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "path"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun path() = path
|
||||
|
||||
@JvmName("-deprecated_headers")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "headers"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun headers() = headers
|
||||
|
||||
@JvmName("-deprecated_response")
|
||||
@Deprecated(
|
||||
message = "moved to val",
|
||||
replaceWith = ReplaceWith(expression = "response"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
fun response() = response
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.gh.gamecenter.mock.mockwebserver3
|
||||
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
||||
import java.util.concurrent.BlockingQueue
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Default dispatcher that processes a script of responses. Populate the script by calling [enqueueResponse].
|
||||
*/
|
||||
open class QueueDispatcher : Dispatcher() {
|
||||
protected val responseQueue: BlockingQueue<MockResponse> = LinkedBlockingQueue()
|
||||
private var failFastResponse: MockResponse? = null
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
// To permit interactive/browser testing, ignore requests for favicons.
|
||||
val requestLine = request.requestLine
|
||||
if (requestLine == "GET /favicon.ico HTTP/1.1") {
|
||||
logger.info("served $requestLine")
|
||||
return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
|
||||
}
|
||||
|
||||
if (failFastResponse != null && responseQueue.peek() == null) {
|
||||
// Fail fast if there's no response queued up.
|
||||
return failFastResponse!!
|
||||
}
|
||||
|
||||
val result = responseQueue.take()
|
||||
|
||||
// If take() returned because we're shutting down, then enqueue another dead letter so that any
|
||||
// other threads waiting on take() will also return.
|
||||
if (result == DEAD_LETTER) responseQueue.add(DEAD_LETTER)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun peek(): MockResponse {
|
||||
return responseQueue.peek() ?: failFastResponse ?: super.peek()
|
||||
}
|
||||
|
||||
open fun enqueueResponse(response: MockResponse) {
|
||||
responseQueue.add(response)
|
||||
}
|
||||
|
||||
open fun clear() {
|
||||
responseQueue.clear()
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
responseQueue.add(DEAD_LETTER)
|
||||
}
|
||||
|
||||
open fun setFailFast(failFast: Boolean) {
|
||||
val failFastResponse = if (failFast) {
|
||||
MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
setFailFast(failFastResponse)
|
||||
}
|
||||
|
||||
open fun setFailFast(failFastResponse: MockResponse?) {
|
||||
this.failFastResponse = failFastResponse
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Enqueued on shutdown to release threads waiting on [dispatch]. Note that this response
|
||||
* isn't transmitted because the connection is closed before this response is returned.
|
||||
*/
|
||||
private val DEAD_LETTER = MockResponse().apply {
|
||||
this.status = "HTTP/1.1 $HTTP_UNAVAILABLE shutting down"
|
||||
}
|
||||
|
||||
private val logger = Logger.getLogger(QueueDispatcher::class.java.name)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.gh.gamecenter.mock.mockwebserver3
|
||||
|
||||
import java.io.IOException
|
||||
import java.net.Inet6Address
|
||||
import java.net.Socket
|
||||
import javax.net.ssl.SSLSocket
|
||||
import okhttp3.Handshake
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.TlsVersion
|
||||
import okio.Buffer
|
||||
|
||||
/** An HTTP request that came into the mock web server. */
|
||||
class RecordedRequest @JvmOverloads constructor(
|
||||
val requestLine: String,
|
||||
|
||||
/** All headers. */
|
||||
val headers: Headers,
|
||||
|
||||
/**
|
||||
* The sizes of the chunks of this request's body, or an empty list if the request's body
|
||||
* was empty or unchunked.
|
||||
*/
|
||||
val chunkSizes: List<Int>,
|
||||
|
||||
/** The total size of the body of this POST request (before truncation).*/
|
||||
val bodySize: Long,
|
||||
|
||||
/** The body of this POST request. This may be truncated. */
|
||||
val body: Buffer,
|
||||
|
||||
/**
|
||||
* The index of this request on its HTTP connection. Since a single HTTP connection may serve
|
||||
* multiple requests, each request is assigned its own sequence number.
|
||||
*/
|
||||
val sequenceNumber: Int,
|
||||
socket: Socket,
|
||||
|
||||
/**
|
||||
* The failure MockWebServer recorded when attempting to decode this request. If, for example,
|
||||
* the inbound request was truncated, this exception will be non-null.
|
||||
*/
|
||||
val failure: IOException? = null
|
||||
) {
|
||||
val method: String?
|
||||
val path: String?
|
||||
|
||||
/**
|
||||
* The TLS handshake of the connection that carried this request, or null if the request was
|
||||
* received without TLS.
|
||||
*/
|
||||
val handshake: Handshake?
|
||||
val requestUrl: HttpUrl?
|
||||
|
||||
@get:JvmName("-deprecated_utf8Body")
|
||||
@Deprecated(
|
||||
message = "Use body.readUtf8()",
|
||||
replaceWith = ReplaceWith("body.readUtf8()"),
|
||||
level = DeprecationLevel.ERROR)
|
||||
val utf8Body: String
|
||||
get() = body.readUtf8()
|
||||
|
||||
/** Returns the connection's TLS version or null if the connection doesn't use SSL. */
|
||||
val tlsVersion: TlsVersion?
|
||||
get() = handshake?.tlsVersion()
|
||||
|
||||
init {
|
||||
if (socket is SSLSocket) {
|
||||
try {
|
||||
this.handshake = Handshake.get(socket.session)
|
||||
} catch (e: IOException) {
|
||||
throw IllegalArgumentException(e)
|
||||
}
|
||||
} else {
|
||||
this.handshake = null
|
||||
}
|
||||
|
||||
if (requestLine.isNotEmpty()) {
|
||||
val methodEnd = requestLine.indexOf(' ')
|
||||
val pathEnd = requestLine.indexOf(' ', methodEnd + 1)
|
||||
this.method = requestLine.substring(0, methodEnd)
|
||||
var path = requestLine.substring(methodEnd + 1, pathEnd)
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/"
|
||||
}
|
||||
this.path = path
|
||||
|
||||
val scheme = if (socket is SSLSocket) "https" else "http"
|
||||
val localPort = socket.localPort
|
||||
val hostAndPort = headers[":authority"]
|
||||
?: headers["Host"]
|
||||
?: when (val inetAddress = socket.localAddress) {
|
||||
is Inet6Address -> "[${inetAddress.hostAddress}]:$localPort"
|
||||
else -> "${inetAddress.hostAddress}:$localPort"
|
||||
}
|
||||
|
||||
// Allow null in failure case to allow for testing bad requests
|
||||
this.requestUrl = HttpUrl.get("$scheme://$hostAndPort$path")
|
||||
} else {
|
||||
this.requestUrl = null
|
||||
this.method = null
|
||||
this.path = null
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "Use body.readUtf8()",
|
||||
replaceWith = ReplaceWith("body.readUtf8()"),
|
||||
level = DeprecationLevel.WARNING)
|
||||
fun getUtf8Body(): String = body.readUtf8()
|
||||
|
||||
/** Returns the first header named [name], or null if no such header exists. */
|
||||
fun getHeader(name: String): String? = headers.values(name).firstOrNull()
|
||||
|
||||
override fun toString(): String = requestLine
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.gh.gamecenter.mock.mockwebserver3
|
||||
|
||||
/**
|
||||
* What should be done with the incoming socket.
|
||||
*
|
||||
* Be careful when using values like [DISCONNECT_AT_END], [SHUTDOWN_INPUT_AT_END]
|
||||
* and [SHUTDOWN_OUTPUT_AT_END] that close a socket after a response, and where there are
|
||||
* follow-up requests. The client is unblocked and free to continue as soon as it has received the
|
||||
* entire response body. If and when the client makes a subsequent request using a pooled socket the
|
||||
* server may not have had time to close the socket. The socket will be closed at an indeterminate
|
||||
* point before or during the second request. It may be closed after client has started sending the
|
||||
* request body. If a request body is not retryable then the client may fail the request, making
|
||||
* client behavior non-deterministic. Add delays in the client to improve the chances that the
|
||||
* server has closed the socket before follow up requests are made.
|
||||
*/
|
||||
enum class SocketPolicy {
|
||||
|
||||
/**
|
||||
* Shutdown [MockWebServer] after writing response.
|
||||
*/
|
||||
SHUTDOWN_SERVER_AFTER_RESPONSE,
|
||||
|
||||
/**
|
||||
* Keep the socket open after the response. This is the default HTTP/1.1 behavior.
|
||||
*/
|
||||
KEEP_OPEN,
|
||||
|
||||
/**
|
||||
* Close the socket after the response. This is the default HTTP/1.0 behavior. For HTTP/2
|
||||
* connections, this sends a [GOAWAYframe](https://tools.ietf.org/html/rfc7540#section-6.8)
|
||||
* immediately after the response and will close the connection when the client's socket
|
||||
* is exhausted.
|
||||
*
|
||||
* See [SocketPolicy] for reasons why this can cause test flakiness and how to avoid it.
|
||||
*/
|
||||
DISCONNECT_AT_END,
|
||||
|
||||
/**
|
||||
* Wrap the socket with SSL at the completion of this request/response pair. Used for CONNECT
|
||||
* messages to tunnel SSL over an HTTP proxy.
|
||||
*/
|
||||
UPGRADE_TO_SSL_AT_END,
|
||||
|
||||
/**
|
||||
* Request immediate close of connection without even reading the request. Use to simulate buggy
|
||||
* SSL servers closing connections in response to unrecognized TLS extensions.
|
||||
*/
|
||||
DISCONNECT_AT_START,
|
||||
|
||||
/**
|
||||
* Close connection after reading the request but before writing the response. Use this to
|
||||
* simulate late connection pool failures.
|
||||
*/
|
||||
DISCONNECT_AFTER_REQUEST,
|
||||
|
||||
/** Close connection after reading half of the request body (if present). */
|
||||
DISCONNECT_DURING_REQUEST_BODY,
|
||||
|
||||
/** Close connection after writing half of the response body (if present). */
|
||||
DISCONNECT_DURING_RESPONSE_BODY,
|
||||
|
||||
/** Don't trust the client during the SSL handshake. */
|
||||
FAIL_HANDSHAKE,
|
||||
|
||||
/**
|
||||
* Shutdown the socket input after sending the response. For testing bad behavior.
|
||||
*
|
||||
* See [SocketPolicy] for reasons why this can cause test flakiness and how to avoid it.
|
||||
*/
|
||||
SHUTDOWN_INPUT_AT_END,
|
||||
|
||||
/**
|
||||
* Shutdown the socket output after sending the response. For testing bad behavior.
|
||||
*
|
||||
* See [SocketPolicy] for reasons why this can cause test flakiness and how to avoid it.
|
||||
*/
|
||||
SHUTDOWN_OUTPUT_AT_END,
|
||||
|
||||
/**
|
||||
* After accepting the connection and doing TLS (if configured) don't do HTTP/1.1 or HTTP/2
|
||||
* framing. Ignore the socket completely until the server is shut down.
|
||||
*/
|
||||
STALL_SOCKET_AT_START,
|
||||
|
||||
/**
|
||||
* Read the request but don't respond to it. Just keep the socket open. For testing read response
|
||||
* header timeout issue.
|
||||
*/
|
||||
NO_RESPONSE,
|
||||
|
||||
/**
|
||||
* Fail HTTP/2 requests without processing them by sending an [MockResponse.getHttp2ErrorCode].
|
||||
*/
|
||||
RESET_STREAM_AT_START,
|
||||
|
||||
/**
|
||||
* Transmit a `HTTP/1.1 100 Continue` response before reading the HTTP request body.
|
||||
* Typically this response is sent when a client makes a request with the header `Expect: 100-continue`.
|
||||
*/
|
||||
EXPECT_CONTINUE,
|
||||
|
||||
/**
|
||||
* Transmit a `HTTP/1.1 100 Continue` response before reading the HTTP request body even
|
||||
* if the client does not send the header `Expect: 100-continue` in its request.
|
||||
*/
|
||||
CONTINUE_ALWAYS
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.gh.gamecenter.mock.mockwebserver3.internal.duplex
|
||||
|
||||
import java.io.IOException
|
||||
import com.gh.gamecenter.mock.mockwebserver3.RecordedRequest
|
||||
import okio.BufferedSink
|
||||
import okio.BufferedSource
|
||||
|
||||
fun interface DuplexResponseBody {
|
||||
@Throws(IOException::class)
|
||||
fun onRequest(request: RecordedRequest, requestBody: BufferedSource, responseBody: BufferedSink)
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.gh.gamecenter.mock.mockwebserver3.internal.duplex
|
||||
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.FutureTask
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
import com.gh.gamecenter.mock.mockwebserver3.RecordedRequest
|
||||
import okhttp3.internal.http2.ErrorCode
|
||||
import okhttp3.internal.http2.Http2Stream
|
||||
import okio.BufferedSink
|
||||
import okio.BufferedSource
|
||||
import okio.buffer
|
||||
import okio.utf8Size
|
||||
import org.junit.Assert
|
||||
|
||||
private typealias Action = (RecordedRequest, BufferedSource, BufferedSink) -> Unit
|
||||
|
||||
/**
|
||||
* A scriptable request/response conversation. Create the script by calling methods like
|
||||
* [receiveRequest] in the sequence they are run.
|
||||
*/
|
||||
class MockDuplexResponseBody : DuplexResponseBody {
|
||||
private val actions = LinkedBlockingQueue<Action>()
|
||||
private val results = LinkedBlockingQueue<FutureTask<Void>>()
|
||||
|
||||
fun receiveRequest(expected: String) = apply {
|
||||
actions += { _, requestBody, _ ->
|
||||
Assert.assertEquals(expected, requestBody.readUtf8(expected.utf8Size()))
|
||||
}
|
||||
}
|
||||
|
||||
fun exhaustRequest() = apply {
|
||||
actions += { _, requestBody, _ -> Assert.assertTrue(requestBody.exhausted()) }
|
||||
}
|
||||
|
||||
fun requestIOException() = apply {
|
||||
actions += { _, requestBody, _ ->
|
||||
try {
|
||||
requestBody.exhausted()
|
||||
Assert.fail()
|
||||
} catch (expected: IOException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmOverloads fun sendResponse(
|
||||
s: String,
|
||||
responseSent: CountDownLatch = CountDownLatch(0)
|
||||
) = apply {
|
||||
actions += { _, _, responseBody ->
|
||||
responseBody.writeUtf8(s)
|
||||
responseBody.flush()
|
||||
responseSent.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
fun exhaustResponse() = apply {
|
||||
actions += { _, _, responseBody -> responseBody.close() }
|
||||
}
|
||||
|
||||
fun sleep(duration: Long, unit: TimeUnit) = apply {
|
||||
actions += { _, _, _ -> Thread.sleep(unit.toMillis(duration)) }
|
||||
}
|
||||
|
||||
override fun onRequest(
|
||||
request: RecordedRequest,
|
||||
requestBody: BufferedSource,
|
||||
responseBody: BufferedSink
|
||||
) {
|
||||
val futureTask = FutureTask<Void> {
|
||||
while (true) {
|
||||
val action = actions.poll() ?: break
|
||||
action(request, requestBody, responseBody)
|
||||
}
|
||||
return@FutureTask null
|
||||
}
|
||||
results.add(futureTask)
|
||||
futureTask.run()
|
||||
}
|
||||
|
||||
/** Returns once the duplex conversation completes successfully. */
|
||||
fun awaitSuccess() {
|
||||
val futureTask = results.poll(5, TimeUnit.SECONDS)
|
||||
?: throw AssertionError("no onRequest call received")
|
||||
futureTask.get(5, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.gh.gamecenter.mock.okhttp3.internal.duplex
|
||||
|
||||
import com.gh.gamecenter.mock.mockwebserver3.MockResponse
|
||||
import com.gh.gamecenter.mock.mockwebserver3.internal.duplex.DuplexResponseBody
|
||||
|
||||
|
||||
/**
|
||||
* Internal access to MockWebServer APIs. Don't use this, don't use internal, these APIs are not
|
||||
* stable.
|
||||
*/
|
||||
abstract class MwsDuplexAccess {
|
||||
|
||||
abstract fun setBody(mockResponse: MockResponse, duplexResponseBody: DuplexResponseBody)
|
||||
|
||||
companion object {
|
||||
@JvmField var instance: MwsDuplexAccess? = null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user