新增本地 mock 接口返回数据

This commit is contained in:
郭涛
2022-10-17 13:56:46 +08:00
committed by 陈君陶
parent 5a560869d3
commit ebbbc618b9
23 changed files with 2522 additions and 1 deletions

View File

@ -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;
}

View File

@ -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)
}
}

View 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")
)
}
)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()
)
}
}
}

View File

@ -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()
}
}

View File

@ -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"
}

View File

@ -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() {}
}

View File

@ -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"
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}
}