From 733178fbdca2b0600cd22f08d732a0026df06d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Wed, 10 Feb 2021 19:09:36 +0100 Subject: [PATCH 01/10] Add custom body encoding --- library-no-op/api/library-no-op.api | 6 + .../chuckerteam/chucker/api/BodyDecoder.kt | 17 ++ .../chucker/api/ChuckerInterceptor.kt | 2 + library/api/library.api | 6 + .../chuckerteam/chucker/api/BodyDecoder.kt | 29 +++ .../chucker/api/ChuckerInterceptor.kt | 18 +- .../chucker/internal/support/OkioUtils.kt | 7 + .../internal/support/PlainTextDecoder.kt | 28 +++ .../internal/support/RequestProcessor.kt | 38 ++-- .../internal/support/ResponseProcessor.kt | 34 ++-- .../support/TransactionDetailsSharable.kt | 22 ++- .../transaction/TransactionPayloadFragment.kt | 14 +- .../chucker/ChuckerInterceptorDelegate.kt | 3 + .../chucker/api/ChuckerInterceptorTest.kt | 177 ++++++++++++++++++ 14 files changed, 354 insertions(+), 47 deletions(-) create mode 100644 library-no-op/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt create mode 100644 library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt diff --git a/library-no-op/api/library-no-op.api b/library-no-op/api/library-no-op.api index e5c93dab7..97007b4a2 100644 --- a/library-no-op/api/library-no-op.api +++ b/library-no-op/api/library-no-op.api @@ -1,3 +1,8 @@ +public abstract interface class com/chuckerteam/chucker/api/BodyDecoder { + public abstract fun decodeRequest (Lokhttp3/Request;Lokio/ByteString;)Ljava/lang/String; + public abstract fun decodeResponse (Lokhttp3/Response;Lokio/ByteString;)Ljava/lang/String; +} + public final class com/chuckerteam/chucker/api/Chucker { public static final field INSTANCE Lcom/chuckerteam/chucker/api/Chucker; public static final fun dismissNotifications (Landroid/content/Context;)V @@ -23,6 +28,7 @@ public final class com/chuckerteam/chucker/api/ChuckerInterceptor : okhttp3/Inte public final class com/chuckerteam/chucker/api/ChuckerInterceptor$Builder { public fun (Landroid/content/Context;)V + public final fun addBodyDecoder (Ljava/lang/Object;)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder; public final fun alwaysReadResponseBody (Z)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder; public final fun build ()Lcom/chuckerteam/chucker/api/ChuckerInterceptor; public final fun collector (Lcom/chuckerteam/chucker/api/ChuckerCollector;)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder; diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt new file mode 100644 index 000000000..a295a77c3 --- /dev/null +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt @@ -0,0 +1,17 @@ +package com.chuckerteam.chucker.api + +import okhttp3.Request +import okhttp3.Response +import okio.ByteString +import okio.IOException + +/** + * No-op declaration + */ +public interface BodyDecoder { + @Throws(IOException::class) + public fun decodeRequest(request: Request, body: ByteString): String? + + @Throws(IOException::class) + public fun decodeResponse(response: Response, body: ByteString): String? +} diff --git a/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt b/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt index 12c0ee319..1396d5b71 100644 --- a/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt +++ b/library-no-op/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt @@ -42,6 +42,8 @@ public class ChuckerInterceptor private constructor( public fun alwaysReadResponseBody(enable: Boolean): Builder = this + public fun addBodyDecoder(decoder: Any): Builder = this + public fun build(): ChuckerInterceptor = ChuckerInterceptor(this) } } diff --git a/library/api/library.api b/library/api/library.api index 23a442650..bdf6b1401 100644 --- a/library/api/library.api +++ b/library/api/library.api @@ -1,3 +1,8 @@ +public abstract interface class com/chuckerteam/chucker/api/BodyDecoder { + public abstract fun decodeRequest (Lokhttp3/Request;Lokio/ByteString;)Ljava/lang/String; + public abstract fun decodeResponse (Lokhttp3/Response;Lokio/ByteString;)Ljava/lang/String; +} + public final class com/chuckerteam/chucker/api/Chucker { public static final field INSTANCE Lcom/chuckerteam/chucker/api/Chucker; public static final fun dismissNotifications (Landroid/content/Context;)V @@ -23,6 +28,7 @@ public final class com/chuckerteam/chucker/api/ChuckerInterceptor : okhttp3/Inte public final class com/chuckerteam/chucker/api/ChuckerInterceptor$Builder { public fun (Landroid/content/Context;)V + public final fun addBodyDecoder (Lcom/chuckerteam/chucker/api/BodyDecoder;)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder; public final fun alwaysReadResponseBody (Z)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder; public final fun build ()Lcom/chuckerteam/chucker/api/ChuckerInterceptor; public final fun collector (Lcom/chuckerteam/chucker/api/ChuckerCollector;)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder; diff --git a/library/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt b/library/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt new file mode 100644 index 000000000..9b52f1222 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/api/BodyDecoder.kt @@ -0,0 +1,29 @@ +package com.chuckerteam.chucker.api + +import okhttp3.Request +import okhttp3.Response +import okio.ByteString +import okio.IOException + +/** + * Decodes HTTP request and response bodies to human–readable texts. + */ +public interface BodyDecoder { + /** + * Returns a text representation of [body] that will be displayed in Chucker UI transaction, + * or `null` if [request] cannot be handled by this decoder. [Body][body] is no longer than + * [max content length][ChuckerInterceptor.Builder.maxContentLength] and is guaranteed to be + * gunzipped even if [request] has gzip header. + */ + @Throws(IOException::class) + public fun decodeRequest(request: Request, body: ByteString): String? + + /** + * Returns a text representation of [body] that will be displayed in Chucker UI transaction, + * or `null` if [response] cannot be handled by this decoder. [Body][body] is no longer than + * [max content length][ChuckerInterceptor.Builder.maxContentLength] and is guaranteed to be + * gunzipped even if [response] has gzip header. + */ + @Throws(IOException::class) + public fun decodeResponse(response: Response, body: ByteString): String? +} diff --git a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt index 2cd600f8b..6974d489a 100755 --- a/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/api/ChuckerInterceptor.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.annotation.VisibleForTesting import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import com.chuckerteam.chucker.internal.support.CacheDirectoryProvider +import com.chuckerteam.chucker.internal.support.PlainTextDecoder import com.chuckerteam.chucker.internal.support.RequestProcessor import com.chuckerteam.chucker.internal.support.ResponseProcessor import okhttp3.Interceptor @@ -31,6 +32,8 @@ public class ChuckerInterceptor private constructor( private val headersToRedact = builder.headersToRedact.toMutableSet() + private val decoders = builder.decoders + BUILT_IN_DECODERS + private val collector = builder.collector ?: ChuckerCollector(builder.context) private val requestProcessor = RequestProcessor( @@ -38,6 +41,7 @@ public class ChuckerInterceptor private constructor( collector, builder.maxContentLength, headersToRedact, + decoders, ) private val responseProcessor = ResponseProcessor( @@ -45,7 +49,8 @@ public class ChuckerInterceptor private constructor( builder.cacheDirectoryProvider ?: CacheDirectoryProvider { builder.context.filesDir }, builder.maxContentLength, headersToRedact, - builder.alwaysReadResponseBody + builder.alwaysReadResponseBody, + decoders, ) /** Adds [headerName] into [headersToRedact] */ @@ -82,6 +87,7 @@ public class ChuckerInterceptor private constructor( internal var cacheDirectoryProvider: CacheDirectoryProvider? = null internal var alwaysReadResponseBody = false internal var headersToRedact = emptySet() + internal var decoders = emptyList() /** * Sets the [ChuckerCollector] to customize data retention. @@ -126,6 +132,14 @@ public class ChuckerInterceptor private constructor( this.alwaysReadResponseBody = enable } + /** + * Adds a [decoder] into Chucker's processing pipeline. Decoders are applied in an order they were added in. + * Request and response bodies are set to the first non–null value returned by any of the decoders. + */ + public fun addBodyDecoder(decoder: BodyDecoder): Builder = apply { + this.decoders += decoder + } + /** * Sets provider of a directory where Chucker will save temporary responses * before processing them. @@ -143,5 +157,7 @@ public class ChuckerInterceptor private constructor( private companion object { private const val MAX_CONTENT_LENGTH = 250_000L + + private val BUILT_IN_DECODERS = listOf(PlainTextDecoder) } } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/OkioUtils.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/OkioUtils.kt index fe7324691..97bd598a5 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/OkioUtils.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/OkioUtils.kt @@ -1,6 +1,7 @@ package com.chuckerteam.chucker.internal.support import okio.Buffer +import okio.ByteString import java.io.EOFException import kotlin.math.min @@ -23,4 +24,10 @@ internal val Buffer.isProbablyPlainText false // Truncated UTF-8 sequence } +internal val ByteString.isProbablyPlainText: Boolean + get() { + val byteCount = min(size, MAX_PREFIX_SIZE.toInt()) + return Buffer().write(this, offset = 0, byteCount).isProbablyPlainText + } + private fun Int.isPlainTextChar() = Character.isWhitespace(this) || !Character.isISOControl(this) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt new file mode 100644 index 000000000..1f3e1a950 --- /dev/null +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt @@ -0,0 +1,28 @@ +package com.chuckerteam.chucker.internal.support + +import com.chuckerteam.chucker.api.BodyDecoder +import okhttp3.Headers +import okhttp3.MediaType +import okhttp3.Request +import okhttp3.Response +import okio.ByteString +import kotlin.text.Charsets.UTF_8 + +internal object PlainTextDecoder : BodyDecoder { + override fun decodeRequest( + request: Request, + body: ByteString, + ) = body.tryDecodeAsPlainText(request.headers, request.body?.contentType()) + + override fun decodeResponse( + response: Response, + body: ByteString, + ) = body.tryDecodeAsPlainText(response.headers, response.body?.contentType()) + + private fun ByteString.tryDecodeAsPlainText( + headers: Headers, + contentType: MediaType?, + ) = if (headers.hasSupportedContentEncoding && isProbablyPlainText) { + string(contentType?.charset() ?: UTF_8) + } else null +} diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt index 606af1fa9..81c741f0a 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt @@ -2,22 +2,24 @@ package com.chuckerteam.chucker.internal.support import android.content.Context import com.chuckerteam.chucker.R +import com.chuckerteam.chucker.api.BodyDecoder import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import okhttp3.Request import okio.Buffer +import okio.ByteString import okio.IOException -import kotlin.text.Charsets.UTF_8 internal class RequestProcessor( private val context: Context, private val collector: ChuckerCollector, private val maxContentLength: Long, private val headersToRedact: Set, + private val bodyDecoders: List, ) { fun process(request: Request, transaction: HttpTransaction) { processMetadata(request, transaction) - processBody(request, transaction) + processPayload(request, transaction) collector.onRequestSent(transaction) } @@ -33,7 +35,7 @@ internal class RequestProcessor( } } - private fun processBody(request: Request, transaction: HttpTransaction) { + private fun processPayload(request: Request, transaction: HttpTransaction) { val body = request.body ?: return if (body.isOneShot()) { Logger.info("Skipping one shot request body") @@ -44,10 +46,6 @@ internal class RequestProcessor( return } - if (!request.headers.hasSupportedContentEncoding) { - return - } - val requestSource = try { Buffer().apply { body.writeTo(this) } } catch (e: IOException) { @@ -57,18 +55,24 @@ internal class RequestProcessor( val limitingSource = LimitingSource(requestSource.uncompress(request.headers), maxContentLength) val contentBuffer = Buffer().apply { limitingSource.use { writeAll(it) } } - if (!contentBuffer.isProbablyPlainText) { - return - } + transaction.isRequestBodyPlainText = contentBuffer.isProbablyPlainText - transaction.isRequestBodyPlainText = true - try { - transaction.requestBody = contentBuffer.readString(body.contentType()?.charset() ?: UTF_8) - } catch (e: IOException) { - Logger.error("Failed to process request payload", e) - } - if (limitingSource.isThresholdReached) { + val decodedContent = decodePayload(request, contentBuffer.readByteString()) + Logger.info("$decodedContent") + transaction.requestBody = decodedContent + if (decodedContent != null && limitingSource.isThresholdReached) { transaction.requestBody += context.getString(R.string.chucker_body_content_truncated) } } + + private fun decodePayload(request: Request, body: ByteString) = bodyDecoders.asSequence() + .mapNotNull { decoder -> + try { + Logger.info("Decoding with: $decoder") + decoder.decodeRequest(request, body) + } catch (e: IOException) { + Logger.error("Failed to process request payload", e) + null + } + }.firstOrNull() } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt index 1cb00e82f..02e1d10d0 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt @@ -1,10 +1,13 @@ package com.chuckerteam.chucker.internal.support +import com.chuckerteam.chucker.api.BodyDecoder import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import okhttp3.Response import okhttp3.ResponseBody.Companion.asResponseBody import okio.Buffer +import okio.ByteString +import okio.IOException import okio.Source import okio.buffer import okio.source @@ -16,6 +19,7 @@ internal class ResponseProcessor( private val maxContentLength: Long, private val headersToRedact: Set, private val alwaysReadResponseBody: Boolean, + private val bodyDecoders: List, ) { fun process(response: Response, transaction: HttpTransaction): Response { processResponseMetadata(response, transaction) @@ -78,26 +82,32 @@ internal class ResponseProcessor( } } - private fun processResponsePayload(response: Response, payload: Buffer, transaction: HttpTransaction) { + private fun processPayload(response: Response, payload: Buffer, transaction: HttpTransaction) { val responseBody = response.body ?: return val contentType = responseBody.contentType() - val charset = contentType?.charset() ?: Charsets.UTF_8 - if (payload.isProbablyPlainText) { - transaction.isResponseBodyPlainText = true - if (payload.size != 0L) { - transaction.responseBody = payload.readString(charset) - } - } else { - val isImageContentType = contentType?.toString()?.contains(CONTENT_TYPE_IMAGE, ignoreCase = true) == true - - if (isImageContentType && (payload.size < MAX_BLOB_SIZE)) { + val isImageContentType = contentType?.toString()?.contains(CONTENT_TYPE_IMAGE, ignoreCase = true) == true + if (isImageContentType) { + if (payload.size < MAX_BLOB_SIZE) { transaction.responseImageData = payload.readByteArray() } + } else if (payload.size != 0L) { + transaction.isResponseBodyPlainText = payload.isProbablyPlainText + transaction.responseBody = decodePayload(response, payload.readByteString()) } } + private fun decodePayload(response: Response, body: ByteString) = bodyDecoders.asSequence() + .mapNotNull { decoder -> + try { + decoder.decodeResponse(response, body) + } catch (e: IOException) { + Logger.error("Failed to process response payload", e) + null + } + }.firstOrNull() + private inner class ResponseReportingSinkCallback( private val response: Response, private val transaction: HttpTransaction, @@ -105,7 +115,7 @@ internal class ResponseProcessor( override fun onClosed(file: File?, sourceByteCount: Long) { file?.readResponsePayload()?.let { payload -> - processResponsePayload(response, payload, transaction) + processPayload(response, payload, transaction) } transaction.responsePayloadSize = sourceByteCount collector.onResponseReceived(transaction) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharable.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharable.kt index 2cc719658..586b03949 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharable.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharable.kt @@ -37,14 +37,15 @@ internal class TransactionDetailsSharable( } writeUtf8( - if (transaction.isRequestBodyPlainText) { - if (transaction.requestBody.isNullOrBlank()) { - context.getString(R.string.chucker_body_empty) + if (transaction.requestBody.isNullOrBlank()) { + val resId = if (!transaction.isRequestBodyPlainText) { + R.string.chucker_body_omitted } else { - transaction.getFormattedRequestBody() + R.string.chucker_body_empty } + context.getString(resId) } else { - context.getString(R.string.chucker_body_omitted) + transaction.getFormattedRequestBody() } ) @@ -59,14 +60,15 @@ internal class TransactionDetailsSharable( } writeUtf8( - if (transaction.isResponseBodyPlainText) { - if (transaction.responseBody.isNullOrBlank()) { - context.getString(R.string.chucker_body_empty) + if (transaction.responseBody.isNullOrBlank()) { + val resId = if (!transaction.isResponseBodyPlainText) { + R.string.chucker_body_omitted } else { - transaction.getFormattedResponseBody() + R.string.chucker_body_empty } + context.getString(resId) } else { - context.getString(R.string.chucker_body_omitted) + transaction.getFormattedResponseBody() } ) } diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt index 03a3ac9ac..b23d5c1b3 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt @@ -268,16 +268,16 @@ internal class TransactionPayloadFragment : if (type == PayloadType.RESPONSE && responseBitmap != null) { val bitmapLuminance = responseBitmap.calculateLuminance() result.add(TransactionPayloadItem.ImageItem(responseBitmap, bitmapLuminance)) - } else if (!isBodyPlainText) { - requireContext().getString(R.string.chucker_body_omitted).let { - result.add(TransactionPayloadItem.BodyLineItem(SpannableStringBuilder.valueOf(it))) - } - } else { - if (bodyString.isNotBlank()) { - bodyString.lines().forEach { + } else if (bodyString.isBlank()) { + if (!isBodyPlainText) { + requireContext().getString(R.string.chucker_body_omitted).let { result.add(TransactionPayloadItem.BodyLineItem(SpannableStringBuilder.valueOf(it))) } } + } else { + bodyString.lines().forEach { + result.add(TransactionPayloadItem.BodyLineItem(SpannableStringBuilder.valueOf(it))) + } } return@withContext result } diff --git a/library/src/test/java/com/chuckerteam/chucker/ChuckerInterceptorDelegate.kt b/library/src/test/java/com/chuckerteam/chucker/ChuckerInterceptorDelegate.kt index e2129ec6c..f9877e111 100644 --- a/library/src/test/java/com/chuckerteam/chucker/ChuckerInterceptorDelegate.kt +++ b/library/src/test/java/com/chuckerteam/chucker/ChuckerInterceptorDelegate.kt @@ -1,6 +1,7 @@ package com.chuckerteam.chucker import android.content.Context +import com.chuckerteam.chucker.api.BodyDecoder import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.internal.data.entity.HttpTransaction @@ -17,6 +18,7 @@ internal class ChuckerInterceptorDelegate( headersToRedact: Set = emptySet(), alwaysReadResponseBody: Boolean = false, cacheDirectoryProvider: CacheDirectoryProvider, + decoders: List = emptyList(), ) : Interceptor { private val idGenerator = AtomicLong() private val transactions = CopyOnWriteArrayList() @@ -39,6 +41,7 @@ internal class ChuckerInterceptorDelegate( .redactHeaders(headersToRedact) .alwaysReadResponseBody(alwaysReadResponseBody) .cacheDirectorProvider(cacheDirectoryProvider) + .apply { decoders.forEach(::addBodyDecoder) } .build() internal fun expectTransaction(): HttpTransaction { diff --git a/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt b/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt index 94eb47441..eec3db5c0 100644 --- a/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt @@ -16,6 +16,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy @@ -24,6 +25,7 @@ import okio.BufferedSink import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import okio.GzipSink +import okio.IOException import okio.buffer import org.junit.Rule import org.junit.jupiter.api.assertThrows @@ -566,4 +568,179 @@ internal class ChuckerInterceptorTest { } private fun RequestBody.toServerRequest() = Request.Builder().url(serverUrl).post(this).build() + + @ParameterizedTest + @EnumSource + fun customBodyDecoder_doesNotChangeRequestBody(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(ReversingDecoder()), + ) + val client = factory.create(chuckerInterceptor) + server.enqueue(MockResponse()) + + val request = "Hello, world!".toRequestBody().toServerRequest() + client.newCall(request).execute().readByteStringBody() + val serverRequestContent = server.takeRequest().body.readByteString() + + assertThat(serverRequestContent.utf8()).isEqualTo("Hello, world!") + } + + @ParameterizedTest + @EnumSource + fun customBodyDecoder_doesNotChangeResponseBody(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(ReversingDecoder()), + ) + val client = factory.create(chuckerInterceptor) + + val body = Buffer().apply { writeUtf8("Hello, world!") } + server.enqueue(MockResponse().setBody(body)) + val request = Request.Builder().url(serverUrl).build() + + val responseBody = client.newCall(request).execute().readByteStringBody()!! + + assertThat(responseBody.utf8()).isEqualTo("Hello, world!") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun customBodyDecoder_isUsedForDecoding(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("Request") + assertThat(transaction.responseBody).isEqualTo("Response") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun bodyDecoders_areUsedInAppliedOrder(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(ReversingDecoder(), LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("olleH") + assertThat(transaction.responseBody).isEqualTo("eybdooG") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun nextBodyDecoder_isUsed_whenPreviousDoesNotDecode(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(NoOpDecoder(), LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("Request") + assertThat(transaction.responseBody).isEqualTo("Response") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun bodyDecoder_canThrowIoExceptions(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(IoThrowingDecoder(), LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("Request") + assertThat(transaction.responseBody).isEqualTo("Response") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun bodyDecoders_areAppliedLazily(factory: ClientFactory) { + val statefulDecoder = StatefulDecoder() + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(LiteralBodyDecoder(), statefulDecoder), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + assertThat(statefulDecoder.didDecodeRequest).isFalse() + assertThat(statefulDecoder.didDecodeResponse).isFalse() + } + + private class LiteralBodyDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString) = "Request" + override fun decodeResponse(response: Response, body: ByteString) = "Response" + } + + private class ReversingDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString) = body.utf8().reversed() + override fun decodeResponse(response: Response, body: ByteString) = body.utf8().reversed() + } + + private class NoOpDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString): String? = null + override fun decodeResponse(response: Response, body: ByteString): String? = null + } + + private class IoThrowingDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString) = throw IOException("Request") + override fun decodeResponse(response: Response, body: ByteString) = throw IOException("Response") + } + + private class StatefulDecoder : BodyDecoder { + var didDecodeRequest = false + + override fun decodeRequest(request: Request, body: ByteString): String { + didDecodeRequest = true + return "" + } + + var didDecodeResponse = false + + override fun decodeResponse(response: Response, body: ByteString): String { + didDecodeResponse = true + return "" + } + } } From f0b44c602dcb216e3a90b0d55345cc7e82f96a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Wed, 10 Feb 2021 20:17:15 +0100 Subject: [PATCH 02/10] Add custom body encoder to the sample app --- build.gradle | 2 ++ gradle/kotlin-static-analysis.gradle | 2 +- sample/build.gradle | 9 +++++ .../chucker/sample/HttpBinClient.kt | 34 ++++++++++++++++++- .../chuckerteam/chucker/sample/pokemon.proto | 10 ++++++ 5 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 sample/src/main/proto/com/chuckerteam/chucker/sample/pokemon.proto diff --git a/build.gradle b/build.gradle index a2509c1f0..68a9804d0 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ buildscript { gsonVersion = '2.8.6' okhttpVersion = '4.9.1' retrofitVersion = '2.9.0' + wireVersion = '3.6.0' // Debug and quality control binaryCompatibilityValidator = '0.2.4' @@ -51,6 +52,7 @@ buildscript { classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$detektVersion" classpath "org.jlleitschuh.gradle:ktlint-gradle:$ktLintGradleVersion" classpath "org.jetbrains.kotlinx:binary-compatibility-validator:$binaryCompatibilityValidator" + classpath "com.squareup.wire:wire-gradle-plugin:$wireVersion" } } apply plugin: 'binary-compatibility-validator' diff --git a/gradle/kotlin-static-analysis.gradle b/gradle/kotlin-static-analysis.gradle index f75383206..0589e6d32 100644 --- a/gradle/kotlin-static-analysis.gradle +++ b/gradle/kotlin-static-analysis.gradle @@ -13,7 +13,7 @@ ktlint { include fileTree("scripts/") } filter { - exclude("**/generated/**") + exclude { element -> element.file.path.contains("generated/") } include("**/kotlin/**") } } diff --git a/sample/build.gradle b/sample/build.gradle index 5e324a9b1..f8f8779d9 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,7 +1,16 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'com.squareup.wire' + +wire { + kotlin {} +} android { + sourceSets { + getByName("main").java.srcDirs += "$buildDir/generated/source/wire/" + } + compileSdkVersion rootProject.compileSdkVersion defaultConfig { diff --git a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt index 41aa44506..6e202d6f7 100644 --- a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt +++ b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt @@ -1,6 +1,7 @@ package com.chuckerteam.chucker.sample import android.content.Context +import com.chuckerteam.chucker.api.BodyDecoder import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager @@ -13,6 +14,7 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor import okio.Buffer import okio.BufferedSink +import okio.ByteString import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -36,13 +38,14 @@ class HttpBinClient( private val chuckerInterceptor = ChuckerInterceptor.Builder(context) .collector(collector) .maxContentLength(250000L) + .addBodyDecoder(PokemonProtoBodyDecoder()) .redactHeaders(emptySet()) .build() private val httpClient = OkHttpClient.Builder() // Add a ChuckerInterceptor instance to your OkHttp client - .addInterceptor(chuckerInterceptor) + .addNetworkInterceptor(chuckerInterceptor) .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build() @@ -102,6 +105,7 @@ class HttpBinClient( downloadSampleImage(colorHex = "fff") downloadSampleImage(colorHex = "000") getResponsePartially() + getProtoResponse() } private fun oneShotRequestBody() = object : RequestBody() { @@ -145,4 +149,32 @@ class HttpBinClient( } ) } + + private fun getProtoResponse() { + val pokemon = Pokemon("Pikachu", level = 99) + val body = pokemon.encodeByteString().toRequestBody("application/protobuf".toMediaType()) + val request = Request.Builder() + .url("https://postman-echo.com/post") + .post(body) + .build() + httpClient.newCall(request).enqueue( + object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: IOException) = Unit + + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + response.body?.source()?.use { it.readByteString() } + } + } + ) + } + + private class PokemonProtoBodyDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString): String? { + return if (request.url.host.contains("postman", ignoreCase = true)) { + Pokemon.ADAPTER.decode(body).toString() + } else null + } + + override fun decodeResponse(response: okhttp3.Response, body: ByteString): String? = null + } } diff --git a/sample/src/main/proto/com/chuckerteam/chucker/sample/pokemon.proto b/sample/src/main/proto/com/chuckerteam/chucker/sample/pokemon.proto new file mode 100644 index 000000000..9436d6adc --- /dev/null +++ b/sample/src/main/proto/com/chuckerteam/chucker/sample/pokemon.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package com.chuckerteam.chucker.sample; + +option java_package = "com.chuckerteam.chucker.sample"; + +message Pokemon { + string name = 1; + int32 level = 2; +} From 32b2bcfb4b3a1ab1f148c8ae76197baef7244ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Wed, 10 Feb 2021 23:50:30 +0100 Subject: [PATCH 03/10] Update documentation --- CHANGELOG.md | 21 ++++++++++++--------- README.md | 25 ++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 922c52c70..099ee902d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ # Change Log -This file follows [Keepachangelog](https://keepachangelog.com/) format. +This file follows [Keepachangelog](https://keepachangelog.com/) format. Please add your entries according to this format. ## Unreleased +### Added +* Decoding of request and response bodies can now be customized. In order to do this a `BodyDecoder` interface needs to be implemented and installed in the `ChuckerInterceptor` via `ChuckerInterceptor.addBinaryDecoder(decoder)` method. Decoded bodies are then displayed in the Chucker UI. + ### Fixed * Fixed not setting request body type correctly [#538]. @@ -46,8 +49,8 @@ Please add your entries according to this format. ## Version 3.3.0 *(2020-09-30)* -This is a new minor release with multiple fixes and improvements. -After this release we are starting to work on a new major release 4.x with minSDK 21. +This is a new minor release with multiple fixes and improvements. +After this release we are starting to work on a new major release 4.x with minSDK 21. Bumping minSDK to 21 is required to keep up with [newer versions of OkHttp](https://medium.com/square-corner-blog/okhttp-3-13-requires-android-5-818bb78d07ce). Versions 3.x will be supported for 6 months (till March 2021) getting bugfixes and minor improvements. @@ -55,7 +58,7 @@ Versions 3.x will be supported for 6 months (till March 2021) getting bugfixes a * Added a new flag `alwaysReadResponseBody` into Chucker configuration to read the whole response body even if consumer fails to consume it. * Added port numbers as part of the URL. Numbers appear if they are different from default 80 or 443. -* Chucker now shows partially read application responses properly. Earlier in 3.2.0 such responses didn't appear in the UI. +* Chucker now shows partially read application responses properly. Earlier in 3.2.0 such responses didn't appear in the UI. * Transaction size is defined by actual payload size now, not by `Content-length` header. * Added empty state UI for payloads, so no more guessing if there is some error or the payload is really empty. * Added ability to export list of transactions. @@ -198,7 +201,7 @@ This release was possible thanks to the contribution of: ### This version shouldn't be used as dependency due to [#203](https://github.com/ChuckerTeam/chucker/issues/203). Use 3.1.1 instead. -This is a new minor release of Chucker. Please note that this minor release contains multiple new features (see below) as well as multiple bugfixes. +This is a new minor release of Chucker. Please note that this minor release contains multiple new features (see below) as well as multiple bugfixes. ### Summary of Changes @@ -235,11 +238,11 @@ This is a new minor release of Chucker. Please note that this minor release cont This release was possible thanks to the contribution of: @christopherniksch -@yoavst +@yoavst @psh @kmayoral @vbuberen -@dcampogiani +@dcampogiani @ullas-jain @rakshit444 @olivierperez @@ -249,8 +252,8 @@ This release was possible thanks to the contribution of: @koral-- @redwarp @uOOOO -@sprohaszka -@PaulWoitaschek +@sprohaszka +@PaulWoitaschek ## Version 3.0.1 *(2019-08-16)* diff --git a/README.md b/README.md index ed0645cdf..7d355a2c6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ _A fork of [Chuck](https://github.com/jgilfelt/chuck)_ * [Multi-Window](#multi-window-) * [Configure](#configure-) * [Redact-Header️](#redact-header-️) + * [Decode-Body](#decode-body-) * [Migrating](#migrating-) * [Snapshots](#snapshots-) * [FAQ](#faq-) @@ -65,7 +66,7 @@ android { **That's it!** 🎉 Chucker will now record all HTTP interactions made by your OkHttp client. -Historically, Chucker was distributed through JitPack. +Historically, Chucker was distributed through JitPack. You can find older version of Chucker here: [![JitPack](https://jitpack.io/v/ChuckerTeam/chucker.svg)](https://jitpack.io/#ChuckerTeam/chucker). ## Features 🧰 @@ -79,6 +80,7 @@ Don't forget to check the [changelog](CHANGELOG.md) to have a look at all the ch * **Empty release artifact** 🧼 (no traces of Chucker in your final APK). * Support for body text search with **highlighting** 🕵️‍♂️ * Support for showing **images** in HTTP Responses 🖼 +* Support for custom decoding of HTTP bodies ### Multi-Window 🚪 @@ -112,6 +114,9 @@ val chuckerInterceptor = ChuckerInterceptor.Builder(context) // This is useful in case of parsing errors or when the response body // is closed before being read like in Retrofit with Void and Unit types. .alwaysReadResponseBody(true) + // Use decoder when processing request and response bodies. When multiple decoders are installed they + // are applied in an order they were added. + .addBodyDecoder(decoder) .build() // Don't forget to plug the ChuckerInterceptor inside the OkHttpClient @@ -128,10 +133,28 @@ It is intended for **use during development**, and not in release builds or othe You can redact headers that contain sensitive information by calling `redactHeader(String)` on the `ChuckerInterceptor`. + ```kotlin interceptor.redactHeader("Auth-Token", "User-Session"); ``` +### Decode-Body 📖 + +Chucker by default handles only plain text bodies. If you use a binary format like, for example, Protobuf or Thrift it won't be automatically handled by Chucker. You can, however, install a custom decoder that is capable to read data from different encodings. + +```kotlin +object ProtoDecoder : BinaryDecoder { + fun decodeRequest(request: Request, body: ByteString): String? = if (request.isExpectedProtoRequest) { + decodeProtoBody(body) + } else null + + fun decodeResponse(request: Response, body: ByteString): String? = if (request.isExpectedProtoResponse) { + decodeProtoBody(body) + } else null +} +interceptorBuilder.addBodyDecoder(ProtoDecoder).build() +``` + ## Migrating 🚗 If you're migrating **from [Chuck](https://github.com/jgilfelt/chuck) to Chucker**, please refer to this [migration guide](/docs/migrating-from-chuck.md). From c83fdccd025c1dc58e09d7a576e7366e1cbb3490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Thu, 11 Feb 2021 15:52:52 +0100 Subject: [PATCH 04/10] Remove debug log --- .../com/chuckerteam/chucker/internal/support/RequestProcessor.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt index 81c741f0a..f063259bb 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt @@ -58,7 +58,6 @@ internal class RequestProcessor( transaction.isRequestBodyPlainText = contentBuffer.isProbablyPlainText val decodedContent = decodePayload(request, contentBuffer.readByteString()) - Logger.info("$decodedContent") transaction.requestBody = decodedContent if (decodedContent != null && limitingSource.isThresholdReached) { transaction.requestBody += context.getString(R.string.chucker_body_content_truncated) From f3925390e4c38541395ed27839ac0b9c4843cef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sat, 13 Feb 2021 09:37:27 +0100 Subject: [PATCH 05/10] Enable logging throwable for all levels --- .../main/java/com/chuckerteam/chucker/api/Chucker.kt | 8 ++++---- .../chuckerteam/chucker/internal/support/Logger.kt | 12 ++++++------ .../java/com/chuckerteam/chucker/NoLoggerRule.kt | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/api/Chucker.kt b/library/src/main/java/com/chuckerteam/chucker/api/Chucker.kt index 1bb1e1218..bae02c84b 100644 --- a/library/src/main/java/com/chuckerteam/chucker/api/Chucker.kt +++ b/library/src/main/java/com/chuckerteam/chucker/api/Chucker.kt @@ -41,12 +41,12 @@ public object Chucker { internal var logger: Logger = object : Logger { val TAG = "Chucker" - override fun info(message: String) { - Log.i(TAG, message) + override fun info(message: String, throwable: Throwable?) { + Log.i(TAG, message, throwable) } - override fun warn(message: String) { - Log.w(TAG, message) + override fun warn(message: String, throwable: Throwable?) { + Log.w(TAG, message, throwable) } override fun error(message: String, throwable: Throwable?) { diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/Logger.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/Logger.kt index d41a19924..6327eb3ad 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/Logger.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/Logger.kt @@ -3,19 +3,19 @@ package com.chuckerteam.chucker.internal.support import com.chuckerteam.chucker.api.Chucker internal interface Logger { - fun info(message: String) + fun info(message: String, throwable: Throwable? = null) - fun warn(message: String) + fun warn(message: String, throwable: Throwable? = null) fun error(message: String, throwable: Throwable? = null) companion object : Logger { - override fun info(message: String) { - Chucker.logger.info(message) + override fun info(message: String, throwable: Throwable?) { + Chucker.logger.info(message, throwable) } - override fun warn(message: String) { - Chucker.logger.warn(message) + override fun warn(message: String, throwable: Throwable?) { + Chucker.logger.warn(message, throwable) } override fun error(message: String, throwable: Throwable?) { diff --git a/library/src/test/java/com/chuckerteam/chucker/NoLoggerRule.kt b/library/src/test/java/com/chuckerteam/chucker/NoLoggerRule.kt index 66c728514..15a9dbb6c 100644 --- a/library/src/test/java/com/chuckerteam/chucker/NoLoggerRule.kt +++ b/library/src/test/java/com/chuckerteam/chucker/NoLoggerRule.kt @@ -11,9 +11,9 @@ internal class NoLoggerRule : BeforeAllCallback, AfterAllCallback { override fun beforeAll(context: ExtensionContext) { Chucker.logger = object : Logger { - override fun info(message: String) = Unit + override fun info(message: String, throwable: Throwable?) = Unit - override fun warn(message: String) = Unit + override fun warn(message: String, throwable: Throwable?) = Unit override fun error(message: String, throwable: Throwable?) = Unit } From 8541522aa3a5a7562aa0a446fcccc6d111bcae88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sat, 13 Feb 2021 09:38:34 +0100 Subject: [PATCH 06/10] Change body decoding failure logs --- .../chuckerteam/chucker/internal/support/RequestProcessor.kt | 2 +- .../chuckerteam/chucker/internal/support/ResponseProcessor.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt index f063259bb..9cce5c7f6 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/RequestProcessor.kt @@ -70,7 +70,7 @@ internal class RequestProcessor( Logger.info("Decoding with: $decoder") decoder.decodeRequest(request, body) } catch (e: IOException) { - Logger.error("Failed to process request payload", e) + Logger.warn("Decoder $decoder failed to process request payload", e) null } }.firstOrNull() diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt index 02e1d10d0..719973e03 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/ResponseProcessor.kt @@ -103,7 +103,7 @@ internal class ResponseProcessor( try { decoder.decodeResponse(response, body) } catch (e: IOException) { - Logger.error("Failed to process response payload", e) + Logger.warn("Decoder $decoder failed to process response payload", e) null } }.firstOrNull() From b3bfa993461c226b7f854ab5e4f08146821b3df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sat, 13 Feb 2021 09:47:39 +0100 Subject: [PATCH 07/10] Simplify payload processing logic --- .../transaction/TransactionPayloadFragment.kt | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt index 6223372b5..88c3042ee 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/ui/transaction/TransactionPayloadFragment.kt @@ -245,22 +245,22 @@ internal class TransactionPayloadFragment : ) } - // The body could either be an image, binary encoded or plain text. + // The body could either be an image, plain text, decoded binary or not decoded binary. val responseBitmap = transaction.responseImageBitmap - if (type == PayloadType.RESPONSE && responseBitmap != null) { - val bitmapLuminance = responseBitmap.calculateLuminance() - result.add(TransactionPayloadItem.ImageItem(responseBitmap, bitmapLuminance)) - } else if (bodyString.isBlank()) { - if (!isBodyPlainText) { - requireContext().getString(R.string.chucker_body_omitted).let { - result.add(TransactionPayloadItem.BodyLineItem(SpannableStringBuilder.valueOf(it))) - } + when { + type == PayloadType.RESPONSE && responseBitmap != null -> { + val bitmapLuminance = responseBitmap.calculateLuminance() + result.add(TransactionPayloadItem.ImageItem(responseBitmap, bitmapLuminance)) } - } else { - bodyString.lines().forEach { + bodyString.isNotBlank() -> bodyString.lines().forEach { result.add(TransactionPayloadItem.BodyLineItem(SpannableStringBuilder.valueOf(it))) } + !isBodyPlainText -> { + val text = requireContext().getString(R.string.chucker_body_omitted) + result.add(TransactionPayloadItem.BodyLineItem(SpannableStringBuilder.valueOf(text))) + } } + return@withContext result } } From 039d0b623b45d8ccebe0e3ee5209fea06ee13aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sat, 13 Feb 2021 09:57:36 +0100 Subject: [PATCH 08/10] Move decoder tests to a separate file --- .../api/ChuckerInterceptorDecodingTest.kt | 206 ++++++++++++++++ .../chucker/api/ChuckerInterceptorTest.kt | 226 ++---------------- .../internal/support/DepletingSourceTest.kt | 2 +- .../internal/support/LimitingSourceTest.kt | 2 +- .../support/LiveDataCombineLatestTest.kt | 2 +- .../LiveDataDistinctUntilChangedTest.kt | 2 +- .../internal/support/ReportingSinkTest.kt | 4 +- .../chucker/internal/support/TeeSourceTest.kt | 2 +- .../TransactionCurlCommandSharableTest.kt | 2 +- .../support/TransactionDetailsSharableTest.kt | 2 +- .../TransactionListDetailsSharableTest.kt | 2 +- .../{ => util}/ChuckerInterceptorDelegate.kt | 3 +- .../chuckerteam/chucker/util/ClientFactory.kt | 23 ++ .../chucker/{ => util}/NoLoggerRule.kt | 2 +- .../{ => util}/TestTransactionFactory.kt | 2 +- .../chucker/{ => util}/TestUtils.kt | 7 +- 16 files changed, 263 insertions(+), 226 deletions(-) create mode 100644 library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorDecodingTest.kt rename library/src/test/java/com/chuckerteam/chucker/{ => util}/ChuckerInterceptorDelegate.kt (97%) create mode 100644 library/src/test/java/com/chuckerteam/chucker/util/ClientFactory.kt rename library/src/test/java/com/chuckerteam/chucker/{ => util}/NoLoggerRule.kt (95%) rename library/src/test/java/com/chuckerteam/chucker/{ => util}/TestTransactionFactory.kt (98%) rename library/src/test/java/com/chuckerteam/chucker/{ => util}/TestUtils.kt (88%) diff --git a/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorDecodingTest.kt b/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorDecodingTest.kt new file mode 100644 index 000000000..848173ffb --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorDecodingTest.kt @@ -0,0 +1,206 @@ +package com.chuckerteam.chucker.api + +import com.chuckerteam.chucker.util.ChuckerInterceptorDelegate +import com.chuckerteam.chucker.util.ClientFactory +import com.chuckerteam.chucker.util.NoLoggerRule +import com.chuckerteam.chucker.util.readByteStringBody +import com.chuckerteam.chucker.util.toServerRequest +import com.google.common.truth.Truth.assertThat +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.Buffer +import okio.ByteString +import okio.IOException +import org.junit.Rule +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.io.TempDir +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import java.io.File + +@ExtendWith(NoLoggerRule::class) +internal class ChuckerInterceptorDecodingTest { + @get:Rule val server = MockWebServer() + + private val serverUrl = server.url("/") // Starts server implicitly + + @TempDir lateinit var tempDir: File + + @ParameterizedTest + @EnumSource + fun customBodyDecoder_doesNotChangeRequestBody(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(ReversingDecoder()), + ) + val client = factory.create(chuckerInterceptor) + server.enqueue(MockResponse()) + + val request = "Hello, world!".toRequestBody().toServerRequest(serverUrl) + client.newCall(request).execute().readByteStringBody() + val serverRequestContent = server.takeRequest().body.readByteString() + + assertThat(serverRequestContent.utf8()).isEqualTo("Hello, world!") + } + + @ParameterizedTest + @EnumSource + fun customBodyDecoder_doesNotChangeResponseBody(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(ReversingDecoder()), + ) + val client = factory.create(chuckerInterceptor) + + val body = Buffer().apply { writeUtf8("Hello, world!") } + server.enqueue(MockResponse().setBody(body)) + val request = Request.Builder().url(serverUrl).build() + + val responseBody = client.newCall(request).execute().readByteStringBody()!! + + assertThat(responseBody.utf8()).isEqualTo("Hello, world!") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun customBodyDecoder_isUsedForDecoding(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("Request") + assertThat(transaction.responseBody).isEqualTo("Response") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun bodyDecoders_areUsedInAppliedOrder(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(ReversingDecoder(), LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("olleH") + assertThat(transaction.responseBody).isEqualTo("eybdooG") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun nextBodyDecoder_isUsed_whenPreviousDoesNotDecode(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(NoOpDecoder(), LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("Request") + assertThat(transaction.responseBody).isEqualTo("Response") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun bodyDecoder_canThrowIoExceptions(factory: ClientFactory) { + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(IoThrowingDecoder(), LiteralBodyDecoder()), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + val transaction = chuckerInterceptor.expectTransaction() + + assertThat(transaction.requestBody).isEqualTo("Request") + assertThat(transaction.responseBody).isEqualTo("Response") + } + + @ParameterizedTest + @EnumSource(value = ClientFactory::class) + fun bodyDecoders_areAppliedLazily(factory: ClientFactory) { + val statefulDecoder = StatefulDecoder() + val chuckerInterceptor = ChuckerInterceptorDelegate( + cacheDirectoryProvider = { tempDir }, + decoders = listOf(LiteralBodyDecoder(), statefulDecoder), + ) + val client = factory.create(chuckerInterceptor) + val request = Request.Builder().url(serverUrl) + .post("Hello".toRequestBody()) + .build() + server.enqueue(MockResponse().setBody("Goodbye")) + + client.newCall(request).execute().readByteStringBody() + + assertThat(statefulDecoder.didDecodeRequest).isFalse() + assertThat(statefulDecoder.didDecodeResponse).isFalse() + } + + private class LiteralBodyDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString) = "Request" + override fun decodeResponse(response: Response, body: ByteString) = "Response" + } + + private class ReversingDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString) = body.utf8().reversed() + override fun decodeResponse(response: Response, body: ByteString) = body.utf8().reversed() + } + + private class NoOpDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString): String? = null + override fun decodeResponse(response: Response, body: ByteString): String? = null + } + + private class IoThrowingDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString) = throw IOException("Request") + override fun decodeResponse(response: Response, body: ByteString) = throw IOException("Response") + } + + private class StatefulDecoder : BodyDecoder { + var didDecodeRequest = false + + override fun decodeRequest(request: Request, body: ByteString): String { + didDecodeRequest = true + return "" + } + + var didDecodeResponse = false + + override fun decodeResponse(response: Response, body: ByteString): String { + didDecodeResponse = true + return "" + } + } +} diff --git a/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt b/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt index eec3db5c0..11885e285 100644 --- a/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/api/ChuckerInterceptorTest.kt @@ -1,22 +1,21 @@ package com.chuckerteam.chucker.api -import com.chuckerteam.chucker.ChuckerInterceptorDelegate -import com.chuckerteam.chucker.NoLoggerRule -import com.chuckerteam.chucker.SEGMENT_SIZE -import com.chuckerteam.chucker.getResourceFile -import com.chuckerteam.chucker.readByteStringBody +import com.chuckerteam.chucker.util.ChuckerInterceptorDelegate +import com.chuckerteam.chucker.util.ClientFactory +import com.chuckerteam.chucker.util.NoLoggerRule +import com.chuckerteam.chucker.util.SEGMENT_SIZE +import com.chuckerteam.chucker.util.getResourceFile +import com.chuckerteam.chucker.util.readByteStringBody +import com.chuckerteam.chucker.util.toServerRequest import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import com.google.gson.Gson import com.google.gson.JsonParseException import com.google.gson.stream.JsonReader -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.SocketPolicy @@ -25,7 +24,6 @@ import okio.BufferedSink import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import okio.GzipSink -import okio.IOException import okio.buffer import org.junit.Rule import org.junit.jupiter.api.assertThrows @@ -38,25 +36,6 @@ import java.net.HttpURLConnection.HTTP_NO_CONTENT @ExtendWith(NoLoggerRule::class) internal class ChuckerInterceptorTest { - enum class ClientFactory { - APPLICATION { - override fun create(interceptor: Interceptor): OkHttpClient { - return OkHttpClient.Builder() - .addInterceptor(interceptor) - .build() - } - }, - NETWORK { - override fun create(interceptor: Interceptor): OkHttpClient { - return OkHttpClient.Builder() - .addNetworkInterceptor(interceptor) - .build() - } - }; - - abstract fun create(interceptor: Interceptor): OkHttpClient - } - @get:Rule val server = MockWebServer() private val serverUrl = server.url("/") // Starts server implicitly @@ -419,7 +398,7 @@ internal class ChuckerInterceptorTest { server.enqueue(MockResponse()) val client = factory.create(chuckerInterceptor) - val request = "\u0080".encodeUtf8().toRequestBody().toServerRequest() + val request = "\u0080".encodeUtf8().toRequestBody().toServerRequest(serverUrl) client.newCall(request).execute().body!!.close() val transaction = chuckerInterceptor.expectTransaction() @@ -432,7 +411,7 @@ internal class ChuckerInterceptorTest { server.enqueue(MockResponse()) val client = factory.create(chuckerInterceptor) - val request = "Hello, world!".toRequestBody().toServerRequest() + val request = "Hello, world!".toRequestBody().toServerRequest(serverUrl) client.newCall(request).execute().readByteStringBody() val serverRequestContent = server.takeRequest().body.readByteString() @@ -445,7 +424,7 @@ internal class ChuckerInterceptorTest { server.enqueue(MockResponse()) val client = factory.create(chuckerInterceptor) - val request = "Hello, world!".toRequestBody().toServerRequest() + val request = "Hello, world!".toRequestBody().toServerRequest(serverUrl) client.newCall(request).execute().readByteStringBody() val transaction = chuckerInterceptor.expectTransaction() @@ -463,7 +442,7 @@ internal class ChuckerInterceptorTest { val gzippedBytes = Buffer().apply { GzipSink(this).buffer().use { sink -> sink.writeUtf8("Hello, world!") } }.readByteString() - val request = gzippedBytes.toRequestBody().toServerRequest() + val request = gzippedBytes.toRequestBody().toServerRequest(serverUrl) .newBuilder() .header("Content-Encoding", "gzip") .build() @@ -485,7 +464,7 @@ internal class ChuckerInterceptorTest { ) val client = factory.create(chuckerInterceptor) - val request = "!".repeat(SEGMENT_SIZE.toInt() * 10).toRequestBody().toServerRequest() + val request = "!".repeat(SEGMENT_SIZE.toInt() * 10).toRequestBody().toServerRequest(serverUrl) client.newCall(request).execute().readByteStringBody() val transaction = chuckerInterceptor.expectTransaction() @@ -538,7 +517,7 @@ internal class ChuckerInterceptorTest { override fun writeTo(sink: BufferedSink) { content.readAll(sink) } - }.toServerRequest() + }.toServerRequest(serverUrl) client.newCall(oneShotRequest).execute().readByteStringBody() @@ -559,188 +538,11 @@ internal class ChuckerInterceptorTest { override fun writeTo(sink: BufferedSink) { content.readAll(sink) } - }.toServerRequest() + }.toServerRequest(serverUrl) client.newCall(oneShotRequest).execute().readByteStringBody() val serverRequestContent = server.takeRequest().body.readByteString() assertThat(serverRequestContent.utf8()).isEqualTo("Hello, world!") } - - private fun RequestBody.toServerRequest() = Request.Builder().url(serverUrl).post(this).build() - - @ParameterizedTest - @EnumSource - fun customBodyDecoder_doesNotChangeRequestBody(factory: ClientFactory) { - val chuckerInterceptor = ChuckerInterceptorDelegate( - cacheDirectoryProvider = { tempDir }, - decoders = listOf(ReversingDecoder()), - ) - val client = factory.create(chuckerInterceptor) - server.enqueue(MockResponse()) - - val request = "Hello, world!".toRequestBody().toServerRequest() - client.newCall(request).execute().readByteStringBody() - val serverRequestContent = server.takeRequest().body.readByteString() - - assertThat(serverRequestContent.utf8()).isEqualTo("Hello, world!") - } - - @ParameterizedTest - @EnumSource - fun customBodyDecoder_doesNotChangeResponseBody(factory: ClientFactory) { - val chuckerInterceptor = ChuckerInterceptorDelegate( - cacheDirectoryProvider = { tempDir }, - decoders = listOf(ReversingDecoder()), - ) - val client = factory.create(chuckerInterceptor) - - val body = Buffer().apply { writeUtf8("Hello, world!") } - server.enqueue(MockResponse().setBody(body)) - val request = Request.Builder().url(serverUrl).build() - - val responseBody = client.newCall(request).execute().readByteStringBody()!! - - assertThat(responseBody.utf8()).isEqualTo("Hello, world!") - } - - @ParameterizedTest - @EnumSource(value = ClientFactory::class) - fun customBodyDecoder_isUsedForDecoding(factory: ClientFactory) { - val chuckerInterceptor = ChuckerInterceptorDelegate( - cacheDirectoryProvider = { tempDir }, - decoders = listOf(LiteralBodyDecoder()), - ) - val client = factory.create(chuckerInterceptor) - val request = Request.Builder().url(serverUrl) - .post("Hello".toRequestBody()) - .build() - server.enqueue(MockResponse().setBody("Goodbye")) - - client.newCall(request).execute().readByteStringBody() - - val transaction = chuckerInterceptor.expectTransaction() - - assertThat(transaction.requestBody).isEqualTo("Request") - assertThat(transaction.responseBody).isEqualTo("Response") - } - - @ParameterizedTest - @EnumSource(value = ClientFactory::class) - fun bodyDecoders_areUsedInAppliedOrder(factory: ClientFactory) { - val chuckerInterceptor = ChuckerInterceptorDelegate( - cacheDirectoryProvider = { tempDir }, - decoders = listOf(ReversingDecoder(), LiteralBodyDecoder()), - ) - val client = factory.create(chuckerInterceptor) - val request = Request.Builder().url(serverUrl) - .post("Hello".toRequestBody()) - .build() - server.enqueue(MockResponse().setBody("Goodbye")) - - client.newCall(request).execute().readByteStringBody() - - val transaction = chuckerInterceptor.expectTransaction() - - assertThat(transaction.requestBody).isEqualTo("olleH") - assertThat(transaction.responseBody).isEqualTo("eybdooG") - } - - @ParameterizedTest - @EnumSource(value = ClientFactory::class) - fun nextBodyDecoder_isUsed_whenPreviousDoesNotDecode(factory: ClientFactory) { - val chuckerInterceptor = ChuckerInterceptorDelegate( - cacheDirectoryProvider = { tempDir }, - decoders = listOf(NoOpDecoder(), LiteralBodyDecoder()), - ) - val client = factory.create(chuckerInterceptor) - val request = Request.Builder().url(serverUrl) - .post("Hello".toRequestBody()) - .build() - server.enqueue(MockResponse().setBody("Goodbye")) - - client.newCall(request).execute().readByteStringBody() - - val transaction = chuckerInterceptor.expectTransaction() - - assertThat(transaction.requestBody).isEqualTo("Request") - assertThat(transaction.responseBody).isEqualTo("Response") - } - - @ParameterizedTest - @EnumSource(value = ClientFactory::class) - fun bodyDecoder_canThrowIoExceptions(factory: ClientFactory) { - val chuckerInterceptor = ChuckerInterceptorDelegate( - cacheDirectoryProvider = { tempDir }, - decoders = listOf(IoThrowingDecoder(), LiteralBodyDecoder()), - ) - val client = factory.create(chuckerInterceptor) - val request = Request.Builder().url(serverUrl) - .post("Hello".toRequestBody()) - .build() - server.enqueue(MockResponse().setBody("Goodbye")) - - client.newCall(request).execute().readByteStringBody() - - val transaction = chuckerInterceptor.expectTransaction() - - assertThat(transaction.requestBody).isEqualTo("Request") - assertThat(transaction.responseBody).isEqualTo("Response") - } - - @ParameterizedTest - @EnumSource(value = ClientFactory::class) - fun bodyDecoders_areAppliedLazily(factory: ClientFactory) { - val statefulDecoder = StatefulDecoder() - val chuckerInterceptor = ChuckerInterceptorDelegate( - cacheDirectoryProvider = { tempDir }, - decoders = listOf(LiteralBodyDecoder(), statefulDecoder), - ) - val client = factory.create(chuckerInterceptor) - val request = Request.Builder().url(serverUrl) - .post("Hello".toRequestBody()) - .build() - server.enqueue(MockResponse().setBody("Goodbye")) - - client.newCall(request).execute().readByteStringBody() - - assertThat(statefulDecoder.didDecodeRequest).isFalse() - assertThat(statefulDecoder.didDecodeResponse).isFalse() - } - - private class LiteralBodyDecoder : BodyDecoder { - override fun decodeRequest(request: Request, body: ByteString) = "Request" - override fun decodeResponse(response: Response, body: ByteString) = "Response" - } - - private class ReversingDecoder : BodyDecoder { - override fun decodeRequest(request: Request, body: ByteString) = body.utf8().reversed() - override fun decodeResponse(response: Response, body: ByteString) = body.utf8().reversed() - } - - private class NoOpDecoder : BodyDecoder { - override fun decodeRequest(request: Request, body: ByteString): String? = null - override fun decodeResponse(response: Response, body: ByteString): String? = null - } - - private class IoThrowingDecoder : BodyDecoder { - override fun decodeRequest(request: Request, body: ByteString) = throw IOException("Request") - override fun decodeResponse(response: Response, body: ByteString) = throw IOException("Response") - } - - private class StatefulDecoder : BodyDecoder { - var didDecodeRequest = false - - override fun decodeRequest(request: Request, body: ByteString): String { - didDecodeRequest = true - return "" - } - - var didDecodeResponse = false - - override fun decodeResponse(response: Response, body: ByteString): String { - didDecodeResponse = true - return "" - } - } } diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/DepletingSourceTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/DepletingSourceTest.kt index 1ff3536cb..cfa260c2f 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/DepletingSourceTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/DepletingSourceTest.kt @@ -1,6 +1,6 @@ package com.chuckerteam.chucker.internal.support -import com.chuckerteam.chucker.NoLoggerRule +import com.chuckerteam.chucker.util.NoLoggerRule import com.google.common.truth.Truth.assertThat import okio.Buffer import okio.BufferedSource diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/LimitingSourceTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/LimitingSourceTest.kt index f54d719d4..be904f1ab 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/LimitingSourceTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/LimitingSourceTest.kt @@ -1,6 +1,6 @@ package com.chuckerteam.chucker.internal.support -import com.chuckerteam.chucker.SEGMENT_SIZE +import com.chuckerteam.chucker.util.SEGMENT_SIZE import com.google.common.truth.Truth.assertThat import okio.Buffer import okio.buffer diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt index e41660c57..3feaf0f76 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataCombineLatestTest.kt @@ -2,7 +2,7 @@ package com.chuckerteam.chucker.internal.support import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData -import com.chuckerteam.chucker.test +import com.chuckerteam.chucker.util.test import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataDistinctUntilChangedTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataDistinctUntilChangedTest.kt index 744691d60..a9fc740d2 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataDistinctUntilChangedTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/LiveDataDistinctUntilChangedTest.kt @@ -3,7 +3,7 @@ package com.chuckerteam.chucker.internal.support import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.MutableLiveData import androidx.lifecycle.distinctUntilChanged -import com.chuckerteam.chucker.test +import com.chuckerteam.chucker.util.test import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/ReportingSinkTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/ReportingSinkTest.kt index e0549503b..313c6f481 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/ReportingSinkTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/ReportingSinkTest.kt @@ -1,6 +1,6 @@ package com.chuckerteam.chucker.internal.support -import com.chuckerteam.chucker.SEGMENT_SIZE +import com.chuckerteam.chucker.util.SEGMENT_SIZE import com.google.common.truth.Truth.assertThat import okio.Buffer import okio.ByteString @@ -82,7 +82,7 @@ internal class ReportingSinkTest { private class TestReportingCallback : ReportingSink.Callback { private var file: File? = null - val fileContent get() = file?.let { it.source().buffer().readByteString() } + val fileContent get() = file?.source()?.buffer()?.readByteString() var exception: IOException? = null var isSuccess = false private set diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/TeeSourceTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/TeeSourceTest.kt index af4c347d9..136589a27 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/TeeSourceTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/TeeSourceTest.kt @@ -1,6 +1,6 @@ package com.chuckerteam.chucker.internal.support -import com.chuckerteam.chucker.SEGMENT_SIZE +import com.chuckerteam.chucker.util.SEGMENT_SIZE import com.google.common.truth.Truth.assertThat import okio.Buffer import okio.BufferedSource diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionCurlCommandSharableTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionCurlCommandSharableTest.kt index b2f7b6a1a..d887f730f 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionCurlCommandSharableTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionCurlCommandSharableTest.kt @@ -2,8 +2,8 @@ package com.chuckerteam.chucker.internal.support import android.content.Context import androidx.test.core.app.ApplicationProvider -import com.chuckerteam.chucker.TestTransactionFactory import com.chuckerteam.chucker.internal.data.entity.HttpHeader +import com.chuckerteam.chucker.util.TestTransactionFactory import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharableTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharableTest.kt index d56e42404..ea388f380 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharableTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionDetailsSharableTest.kt @@ -2,7 +2,7 @@ package com.chuckerteam.chucker.internal.support import android.content.Context import androidx.test.core.app.ApplicationProvider -import com.chuckerteam.chucker.TestTransactionFactory +import com.chuckerteam.chucker.util.TestTransactionFactory import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionListDetailsSharableTest.kt b/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionListDetailsSharableTest.kt index d792c9599..bdbe6de64 100644 --- a/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionListDetailsSharableTest.kt +++ b/library/src/test/java/com/chuckerteam/chucker/internal/support/TransactionListDetailsSharableTest.kt @@ -2,7 +2,7 @@ package com.chuckerteam.chucker.internal.support import android.content.Context import androidx.test.core.app.ApplicationProvider -import com.chuckerteam.chucker.TestTransactionFactory +import com.chuckerteam.chucker.util.TestTransactionFactory import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith diff --git a/library/src/test/java/com/chuckerteam/chucker/ChuckerInterceptorDelegate.kt b/library/src/test/java/com/chuckerteam/chucker/util/ChuckerInterceptorDelegate.kt similarity index 97% rename from library/src/test/java/com/chuckerteam/chucker/ChuckerInterceptorDelegate.kt rename to library/src/test/java/com/chuckerteam/chucker/util/ChuckerInterceptorDelegate.kt index f9877e111..231c88865 100644 --- a/library/src/test/java/com/chuckerteam/chucker/ChuckerInterceptorDelegate.kt +++ b/library/src/test/java/com/chuckerteam/chucker/util/ChuckerInterceptorDelegate.kt @@ -1,6 +1,7 @@ -package com.chuckerteam.chucker +package com.chuckerteam.chucker.util import android.content.Context +import com.chuckerteam.chucker.R import com.chuckerteam.chucker.api.BodyDecoder import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor diff --git a/library/src/test/java/com/chuckerteam/chucker/util/ClientFactory.kt b/library/src/test/java/com/chuckerteam/chucker/util/ClientFactory.kt new file mode 100644 index 000000000..08eb77946 --- /dev/null +++ b/library/src/test/java/com/chuckerteam/chucker/util/ClientFactory.kt @@ -0,0 +1,23 @@ +package com.chuckerteam.chucker.util + +import okhttp3.Interceptor +import okhttp3.OkHttpClient + +internal enum class ClientFactory { + APPLICATION { + override fun create(interceptor: Interceptor): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + } + }, + NETWORK { + override fun create(interceptor: Interceptor): OkHttpClient { + return OkHttpClient.Builder() + .addNetworkInterceptor(interceptor) + .build() + } + }; + + abstract fun create(interceptor: Interceptor): OkHttpClient +} diff --git a/library/src/test/java/com/chuckerteam/chucker/NoLoggerRule.kt b/library/src/test/java/com/chuckerteam/chucker/util/NoLoggerRule.kt similarity index 95% rename from library/src/test/java/com/chuckerteam/chucker/NoLoggerRule.kt rename to library/src/test/java/com/chuckerteam/chucker/util/NoLoggerRule.kt index 15a9dbb6c..36f9b055f 100644 --- a/library/src/test/java/com/chuckerteam/chucker/NoLoggerRule.kt +++ b/library/src/test/java/com/chuckerteam/chucker/util/NoLoggerRule.kt @@ -1,4 +1,4 @@ -package com.chuckerteam.chucker +package com.chuckerteam.chucker.util import com.chuckerteam.chucker.api.Chucker import com.chuckerteam.chucker.internal.support.Logger diff --git a/library/src/test/java/com/chuckerteam/chucker/TestTransactionFactory.kt b/library/src/test/java/com/chuckerteam/chucker/util/TestTransactionFactory.kt similarity index 98% rename from library/src/test/java/com/chuckerteam/chucker/TestTransactionFactory.kt rename to library/src/test/java/com/chuckerteam/chucker/util/TestTransactionFactory.kt index c683e0c4e..8ea33d8db 100644 --- a/library/src/test/java/com/chuckerteam/chucker/TestTransactionFactory.kt +++ b/library/src/test/java/com/chuckerteam/chucker/util/TestTransactionFactory.kt @@ -1,4 +1,4 @@ -package com.chuckerteam.chucker +package com.chuckerteam.chucker.util import com.chuckerteam.chucker.internal.data.entity.HttpTransaction import java.util.Date diff --git a/library/src/test/java/com/chuckerteam/chucker/TestUtils.kt b/library/src/test/java/com/chuckerteam/chucker/util/TestUtils.kt similarity index 88% rename from library/src/test/java/com/chuckerteam/chucker/TestUtils.kt rename to library/src/test/java/com/chuckerteam/chucker/util/TestUtils.kt index 0da6103b8..8e356c385 100644 --- a/library/src/test/java/com/chuckerteam/chucker/TestUtils.kt +++ b/library/src/test/java/com/chuckerteam/chucker/util/TestUtils.kt @@ -1,8 +1,11 @@ -package com.chuckerteam.chucker +package com.chuckerteam.chucker.util import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.chuckerteam.chucker.internal.support.hasBody +import okhttp3.HttpUrl +import okhttp3.Request +import okhttp3.RequestBody import okhttp3.Response import okio.Buffer import okio.ByteString @@ -32,6 +35,8 @@ internal fun Response.readByteStringBody(length: Long? = null): ByteString? { } } +internal fun RequestBody.toServerRequest(serverUrl: HttpUrl) = Request.Builder().url(serverUrl).post(this).build() + internal fun LiveData.test(test: LiveDataRecord.() -> Unit) { val observer = RecordingObserver() observeForever(observer) From f171e716cfc9dec14c91077de31b9330d3f2eef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sat, 13 Feb 2021 10:01:10 +0100 Subject: [PATCH 09/10] Move sample proto decoder to a separate file --- .../chuckerteam/chucker/sample/HttpBinClient.kt | 12 ------------ .../chucker/sample/PokemonProtoBodyDecoder.kt | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 sample/src/main/java/com/chuckerteam/chucker/sample/PokemonProtoBodyDecoder.kt diff --git a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt index 6e202d6f7..203d05f6d 100644 --- a/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt +++ b/sample/src/main/java/com/chuckerteam/chucker/sample/HttpBinClient.kt @@ -1,7 +1,6 @@ package com.chuckerteam.chucker.sample import android.content.Context -import com.chuckerteam.chucker.api.BodyDecoder import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerInterceptor import com.chuckerteam.chucker.api.RetentionManager @@ -14,7 +13,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.logging.HttpLoggingInterceptor import okio.Buffer import okio.BufferedSink -import okio.ByteString import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -167,14 +165,4 @@ class HttpBinClient( } ) } - - private class PokemonProtoBodyDecoder : BodyDecoder { - override fun decodeRequest(request: Request, body: ByteString): String? { - return if (request.url.host.contains("postman", ignoreCase = true)) { - Pokemon.ADAPTER.decode(body).toString() - } else null - } - - override fun decodeResponse(response: okhttp3.Response, body: ByteString): String? = null - } } diff --git a/sample/src/main/java/com/chuckerteam/chucker/sample/PokemonProtoBodyDecoder.kt b/sample/src/main/java/com/chuckerteam/chucker/sample/PokemonProtoBodyDecoder.kt new file mode 100644 index 000000000..0ad9e4bec --- /dev/null +++ b/sample/src/main/java/com/chuckerteam/chucker/sample/PokemonProtoBodyDecoder.kt @@ -0,0 +1,15 @@ +package com.chuckerteam.chucker.sample + +import com.chuckerteam.chucker.api.BodyDecoder +import okhttp3.Request +import okio.ByteString + +internal class PokemonProtoBodyDecoder : BodyDecoder { + override fun decodeRequest(request: Request, body: ByteString): String? { + return if (request.url.host.contains("postman", ignoreCase = true)) { + Pokemon.ADAPTER.decode(body).toString() + } else null + } + + override fun decodeResponse(response: okhttp3.Response, body: ByteString): String? = null +} From ca6fb44740ad54ffdeb3964ae71b1507cc155261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Sikora?= Date: Sat, 13 Feb 2021 10:01:42 +0100 Subject: [PATCH 10/10] Use braces in else clauses --- README.md | 8 ++++++-- .../chucker/internal/support/PlainTextDecoder.kt | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7d355a2c6..e7603fb9e 100644 --- a/README.md +++ b/README.md @@ -146,11 +146,15 @@ Chucker by default handles only plain text bodies. If you use a binary format li object ProtoDecoder : BinaryDecoder { fun decodeRequest(request: Request, body: ByteString): String? = if (request.isExpectedProtoRequest) { decodeProtoBody(body) - } else null + } else { + null + } fun decodeResponse(request: Response, body: ByteString): String? = if (request.isExpectedProtoResponse) { decodeProtoBody(body) - } else null + } else { + null + } } interceptorBuilder.addBodyDecoder(ProtoDecoder).build() ``` diff --git a/library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt b/library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt index 1f3e1a950..9257c5a2f 100644 --- a/library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt +++ b/library/src/main/java/com/chuckerteam/chucker/internal/support/PlainTextDecoder.kt @@ -24,5 +24,7 @@ internal object PlainTextDecoder : BodyDecoder { contentType: MediaType?, ) = if (headers.hasSupportedContentEncoding && isProbablyPlainText) { string(contentType?.charset() ?: UTF_8) - } else null + } else { + null + } }