From 1bbad70f360a6ccbefd43a8e8d3c4ab6a0705d29 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Wed, 5 Nov 2025 17:16:17 +0100 Subject: [PATCH 1/3] Replace Volley with HttpURLConnection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the legacy Volley library with Android native HttpURLConnection to eliminate external HTTP dependencies and avoid version conflicts with consuming applications. Changes: - Created EncryptedLogHttpClient using HttpURLConnection - Updated EncryptedLogRestClient to use new HTTP client with Dispatchers.IO - Removed Volley dependency from build files - Deleted EncryptedLogUploadRequest (Volley-specific) - Removed RequestQueue initialization from AutomatticEncryptedLogging Benefits: - Zero external HTTP dependencies - No version conflicts with consuming apps - Reduced library size - Same coroutine-based API maintained - All error handling preserved 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- encryptedlogging/build.gradle.kts | 1 - .../AutomatticEncryptedLogging.kt | 20 +---- .../network/EncryptedLogHttpClient.kt | 50 ++++++++++++ .../network/EncryptedLogUploadRequest.kt | 56 ------------- .../encryptedlog/EncryptedLogRestClient.kt | 78 +++++++------------ gradle/libs.versions.toml | 2 - 6 files changed, 83 insertions(+), 124 deletions(-) create mode 100644 encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt delete mode 100644 encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogUploadRequest.kt diff --git a/encryptedlogging/build.gradle.kts b/encryptedlogging/build.gradle.kts index c1bdcde..36e2e34 100644 --- a/encryptedlogging/build.gradle.kts +++ b/encryptedlogging/build.gradle.kts @@ -72,7 +72,6 @@ dependencies { ksp(libs.androidx.room.compiler) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.core) - implementation(libs.volley) testImplementation(libs.androidx.test.core.ktx) testImplementation(libs.assertj.core) testImplementation(libs.junit) diff --git a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/AutomatticEncryptedLogging.kt b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/AutomatticEncryptedLogging.kt index 5d1c0e6..4af3136 100644 --- a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/AutomatticEncryptedLogging.kt +++ b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/AutomatticEncryptedLogging.kt @@ -2,12 +2,9 @@ package com.automattic.encryptedlogging import android.content.Context import android.util.Base64 -import com.android.volley.RequestQueue -import com.android.volley.toolbox.BasicNetwork -import com.android.volley.toolbox.DiskBasedCache -import com.android.volley.toolbox.HurlStack import com.automattic.encryptedlogging.model.encryptedlogging.EncryptedLoggingKey import com.automattic.encryptedlogging.model.encryptedlogging.LogEncrypter +import com.automattic.encryptedlogging.network.EncryptedLogHttpClient import com.automattic.encryptedlogging.network.rest.wpcom.encryptedlog.EncryptedLogRestClient import com.automattic.encryptedlogging.persistence.EncryptedLogDatabase import com.automattic.encryptedlogging.store.EncryptedLogStore @@ -23,22 +20,11 @@ internal class AutomatticEncryptedLogging( encryptedLoggingKey: String, clientSecret: String, ) : EncryptedLogging { - private companion object { - private const val MAX_CACHE_SIZE_IN_BYTES = 1024 * 1024 * 10 - } - private val encryptedLogStore: EncryptedLogStore init { - val cache = DiskBasedCache( - File.createTempFile("tempcache", null), - MAX_CACHE_SIZE_IN_BYTES - ) - val network = BasicNetwork(HurlStack()) - val requestQueue = RequestQueue(cache, network).apply { - start() - } - val encryptedLogRestClient = EncryptedLogRestClient(requestQueue, clientSecret) + val httpClient = EncryptedLogHttpClient(clientSecret) + val encryptedLogRestClient = EncryptedLogRestClient(httpClient) val database = EncryptedLogDatabase.getInstance(context) val logEncrypter = LogEncrypter( EncryptedLoggingKey(Key.fromBytes(Base64.decode(encryptedLoggingKey, Base64.DEFAULT))) diff --git a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt new file mode 100644 index 0000000..16db197 --- /dev/null +++ b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt @@ -0,0 +1,50 @@ +package com.automattic.encryptedlogging.network + +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL + +private const val AUTHORIZATION_HEADER = "Authorization" +private const val CONTENT_TYPE_HEADER = "Content-Type" +private const val CONTENT_TYPE_JSON = "application/json" +private const val UUID_HEADER = "log-uuid" +private const val UPLOAD_URL = "https://public-api.wordpress.com/rest/v1.1/encrypted-logging/" + +internal class EncryptedLogHttpClient( + private val clientSecret: String +) { + @Throws(IOException::class) + fun uploadLog(logUuid: String, contents: String): HttpResponse { + val url = URL(UPLOAD_URL) + val connection = (url.openConnection() as HttpURLConnection).apply { + requestMethod = "POST" + doOutput = true + setRequestProperty(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON) + setRequestProperty(AUTHORIZATION_HEADER, clientSecret) + setRequestProperty(UUID_HEADER, logUuid) + } + + try { + connection.outputStream.use { outputStream -> + outputStream.write(contents.toByteArray()) + outputStream.flush() + } + + val statusCode = connection.responseCode + val responseBody = if (statusCode in 200..299) { + connection.inputStream.bufferedReader().use { it.readText() } + } else { + connection.errorStream?.bufferedReader()?.use { it.readText() } ?: "" + } + + return HttpResponse(statusCode, responseBody) + } finally { + connection.disconnect() + } + } +} + +internal data class HttpResponse( + val statusCode: Int, + val body: String +) diff --git a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogUploadRequest.kt b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogUploadRequest.kt deleted file mode 100644 index 8799a85..0000000 --- a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogUploadRequest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.automattic.encryptedlogging.network - -import com.android.volley.NetworkResponse -import com.android.volley.ParseError -import com.android.volley.Request -import com.android.volley.Response -import com.android.volley.Response.ErrorListener -import com.android.volley.VolleyError -import com.android.volley.toolbox.HttpHeaderParser -import org.json.JSONObject - -private const val AUTHORIZATION_HEADER = "Authorization" -private const val CONTENT_TYPE_HEADER = "Content-Type" -private const val CONTENT_TYPE_JSON = "application/json" -private const val UUID_HEADER = "log-uuid" - -internal class EncryptedLogUploadRequest( - private val logUuid: String, - private val contents: String, - private val clientSecret: String, - private val successListener: Response.Listener, - errorListener: ErrorListener -) : Request(Method.POST, "https://public-api.wordpress.com/rest/v1.1/encrypted-logging/", errorListener) { - override fun getHeaders(): Map { - return mapOf( - CONTENT_TYPE_HEADER to CONTENT_TYPE_JSON, - AUTHORIZATION_HEADER to clientSecret, - UUID_HEADER to logUuid - ) - } - - @Suppress("ForbiddenComment") - override fun getBody(): ByteArray { - // TODO: Max file size is 10MB - maybe we should just handle that in the error callback? - return contents.toByteArray() - } - - @Suppress("TooGenericExceptionCaught", "SwallowedException") - override fun parseNetworkResponse(response: NetworkResponse?): Response { - return try { - Response.success(response, HttpHeaderParser.parseCacheHeaders(response)) - } catch (e: Exception) { - try { - val json = JSONObject(response.toString()) - val errorMessage = json.getString("message") - Response.error(VolleyError(errorMessage)) - } catch (jsonParsingError: Throwable) { - Response.error(ParseError(jsonParsingError)) - } - } - } - - override fun deliverResponse(response: NetworkResponse) { - successListener.onResponse(response) - } -} diff --git a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt index 5a1a115..1c32207 100644 --- a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt +++ b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt @@ -1,70 +1,52 @@ package com.automattic.encryptedlogging.network.rest.wpcom.encryptedlog import android.util.Log -import com.android.volley.NoConnectionError -import com.android.volley.RequestQueue -import com.android.volley.VolleyError -import kotlinx.coroutines.suspendCancellableCoroutine +import com.automattic.encryptedlogging.network.EncryptedLogHttpClient +import com.automattic.encryptedlogging.store.UploadEncryptedLogError +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.json.JSONException import org.json.JSONObject -import com.automattic.encryptedlogging.network.EncryptedLogUploadRequest -import com.automattic.encryptedlogging.store.UploadEncryptedLogError -import kotlin.coroutines.resume +import java.io.IOException private const val INVALID_REQUEST = "invalid-request" private const val TOO_MANY_REQUESTS = "too_many_requests" internal class EncryptedLogRestClient( - private val requestQueue: RequestQueue, - private val clientSecret: String, + private val httpClient: EncryptedLogHttpClient, ) { suspend fun uploadLog(logUuid: String, contents: String): UploadEncryptedLogResult { - return suspendCancellableCoroutine { cont -> - val request = EncryptedLogUploadRequest(logUuid, contents, clientSecret, { - cont.resume(UploadEncryptedLogResult.LogUploaded) - }, { error -> - cont.resume(UploadEncryptedLogResult.LogUploadFailed(mapError(error))) - }) - cont.invokeOnCancellation { request.cancel() } - requestQueue.add(request) + return withContext(Dispatchers.IO) { + try { + val response = httpClient.uploadLog(logUuid, contents) + if (response.statusCode in 200..299) { + UploadEncryptedLogResult.LogUploaded + } else { + UploadEncryptedLogResult.LogUploadFailed(mapError(response.statusCode, response.body)) + } + } catch (e: IOException) { + UploadEncryptedLogResult.LogUploadFailed(UploadEncryptedLogError.NoConnection) + } } } - /** - * { - * "error":"too_many_requests", - * "message":"You're sending too many messages. Please slow down." - * } - * { - * "error":"invalid-request", - * "message":"Invalid UUID: uuids must only contain letters, numbers, dashes, and curly brackets" - * } - */ @Suppress("ReturnCount") - private fun mapError(error: VolleyError): UploadEncryptedLogError { - if (error is NoConnectionError) { - return UploadEncryptedLogError.NoConnection + private fun mapError(statusCode: Int, responseBody: String): UploadEncryptedLogError { + val json = try { + JSONObject(responseBody) + } catch (jsonException: JSONException) { + Log.e(TAG, "Received response not in JSON format: " + jsonException.message) + return UploadEncryptedLogError.Unknown(message = responseBody) } - error.networkResponse?.let { networkResponse -> - val statusCode = networkResponse.statusCode - val dataString = String(networkResponse.data) - val json = try { - JSONObject(dataString) - } catch (jsonException: JSONException) { - Log.e(TAG, "Received response not in JSON format: " + jsonException.message) - return UploadEncryptedLogError.Unknown(message = dataString) - } - val errorMessage = json.getString("message") - json.getString("error").let { errorType -> - if (errorType == INVALID_REQUEST) { - return UploadEncryptedLogError.InvalidRequest - } else if (errorType == TOO_MANY_REQUESTS) { - return UploadEncryptedLogError.TooManyRequests - } + val errorMessage = json.getString("message") + json.getString("error").let { errorType -> + if (errorType == INVALID_REQUEST) { + return UploadEncryptedLogError.InvalidRequest + } else if (errorType == TOO_MANY_REQUESTS) { + return UploadEncryptedLogError.TooManyRequests } - return UploadEncryptedLogError.Unknown(statusCode, errorMessage) } - return UploadEncryptedLogError.Unknown() + return UploadEncryptedLogError.Unknown(statusCode, errorMessage) } companion object { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9872479..906f3ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,6 @@ ksp = "2.1.21-2.0.2" robolectric = "4.14.1" terl-lazysodium-android = "5.2.0@aar" turbine = "1.2.1" -volley = "1.2.1" [libraries] androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } @@ -39,7 +38,6 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } terl-lazysodium-android = { module = "com.goterl:lazysodium-android", version.ref = "terl-lazysodium-android" } turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } -volley = { group = "com.android.volley", name = "volley", version.ref = "volley" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } From d73b8a02c4673a8eff2208d53c218f9d96147ef5 Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Wed, 5 Nov 2025 17:23:14 +0100 Subject: [PATCH 2/3] Add unit tests for EncryptedLogRestClient with MockWebServer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive unit tests to verify EncryptedLogRestClient behavior with the new HttpURLConnection implementation. Changes: - Added MockWebServer test dependency (okhttp3:mockwebserver:3.2.0) - Created EncryptedLogRestClientTest with 7 test cases - Made EncryptedLogHttpClient URL configurable for testing - Tests verify correct request formatting, headers, and error handling Test coverage: - Request headers and body validation - Success response (200) - InvalidRequest error (400) - TooManyRequests error (429) - Unknown error responses (500) - Non-JSON error responses - Network connection failures (IOException) All tests passing with Robolectric test runner. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- encryptedlogging/build.gradle.kts | 1 + .../network/EncryptedLogHttpClient.kt | 7 +- .../EncryptedLogRestClientTest.kt | 148 ++++++++++++++++++ gradle/libs.versions.toml | 2 + 4 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt diff --git a/encryptedlogging/build.gradle.kts b/encryptedlogging/build.gradle.kts index 36e2e34..7e210f8 100644 --- a/encryptedlogging/build.gradle.kts +++ b/encryptedlogging/build.gradle.kts @@ -76,6 +76,7 @@ dependencies { testImplementation(libs.assertj.core) testImplementation(libs.junit) testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockwebserver) testImplementation(libs.robolectric) androidTestImplementation(libs.assertj.core) androidTestImplementation(libs.androidx.test.ext.junit) diff --git a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt index 16db197..4359223 100644 --- a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt +++ b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/EncryptedLogHttpClient.kt @@ -8,14 +8,15 @@ private const val AUTHORIZATION_HEADER = "Authorization" private const val CONTENT_TYPE_HEADER = "Content-Type" private const val CONTENT_TYPE_JSON = "application/json" private const val UUID_HEADER = "log-uuid" -private const val UPLOAD_URL = "https://public-api.wordpress.com/rest/v1.1/encrypted-logging/" +private const val DEFAULT_UPLOAD_URL = "https://public-api.wordpress.com/rest/v1.1/encrypted-logging/" internal class EncryptedLogHttpClient( - private val clientSecret: String + private val clientSecret: String, + private val uploadUrl: String = DEFAULT_UPLOAD_URL ) { @Throws(IOException::class) fun uploadLog(logUuid: String, contents: String): HttpResponse { - val url = URL(UPLOAD_URL) + val url = URL(uploadUrl) val connection = (url.openConnection() as HttpURLConnection).apply { requestMethod = "POST" doOutput = true diff --git a/encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt b/encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt new file mode 100644 index 0000000..23d3bfc --- /dev/null +++ b/encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt @@ -0,0 +1,148 @@ +package com.automattic.encryptedlogging.network.rest.wpcom.encryptedlog + +import com.automattic.encryptedlogging.network.EncryptedLogHttpClient +import com.automattic.encryptedlogging.store.UploadEncryptedLogError +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE) +class EncryptedLogRestClientTest { + private lateinit var mockWebServer: MockWebServer + private lateinit var restClient: EncryptedLogRestClient + + private val testClientSecret = "test-secret" + private val testLogUuid = "test-uuid-123" + private val testContents = "encrypted log contents" + + @Before + fun setup() { + mockWebServer = MockWebServer() + mockWebServer.start() + + val httpClient = EncryptedLogHttpClient( + testClientSecret, + mockWebServer.url("/rest/v1.1/encrypted-logging/").toString() + ) + restClient = EncryptedLogRestClient(httpClient) + } + + @After + fun tearDown() { + mockWebServer.shutdown() + } + + @Test + fun `uploadLog sends correct request with headers and body`() { + mockWebServer.enqueue(MockResponse().setResponseCode(200)) + + runBlocking { + restClient.uploadLog(testLogUuid, testContents) + } + + mockWebServer.takeRequest().let { request -> + assertThat(request.method).isEqualTo("POST") + assertThat(request.path).isEqualTo("/rest/v1.1/encrypted-logging/") + assertThat(request.getHeader("Content-Type")).isEqualTo("application/json") + assertThat(request.getHeader("Authorization")).isEqualTo(testClientSecret) + assertThat(request.getHeader("log-uuid")).isEqualTo(testLogUuid) + assertThat(request.body.readUtf8()).isEqualTo(testContents) + } + } + + @Test + fun `uploadLog returns LogUploaded on success`() { + mockWebServer.enqueue(MockResponse().setResponseCode(200)) + + val result = runBlocking { + restClient.uploadLog(testLogUuid, testContents) + } + + assertThat(result).isInstanceOf(UploadEncryptedLogResult.LogUploaded::class.java) + } + + @Test + fun `uploadLog returns InvalidRequest error for invalid-request error type`() { + mockWebServer.enqueue(MockResponse().setResponseCode(400).setBody(""" + { + "error": "invalid-request", + "message": "Invalid UUID: uuids must only contain letters, numbers, dashes, and curly brackets" + } + """.trimIndent())) + + val result = runBlocking { + restClient.uploadLog(testLogUuid, testContents) + } + + assertThat((result as UploadEncryptedLogResult.LogUploadFailed).error) + .isInstanceOf(UploadEncryptedLogError.InvalidRequest::class.java) + } + + @Test + fun `uploadLog returns TooManyRequests error for too_many_requests error type`() { + mockWebServer.enqueue(MockResponse().setResponseCode(429).setBody(""" + { + "error": "too_many_requests", + "message": "You're sending too many messages. Please slow down." + } + """.trimIndent())) + + val result = runBlocking { + restClient.uploadLog(testLogUuid, testContents) + } + + assertThat((result as UploadEncryptedLogResult.LogUploadFailed).error) + .isInstanceOf(UploadEncryptedLogError.TooManyRequests::class.java) + } + + @Test + fun `uploadLog returns Unknown error for unrecognized error type`() { + mockWebServer.enqueue(MockResponse().setResponseCode(500).setBody(""" + { + "error": "some-other-error", + "message": "Some other error occurred" + } + """.trimIndent())) + + val result = runBlocking { + restClient.uploadLog(testLogUuid, testContents) + } + + ((result as UploadEncryptedLogResult.LogUploadFailed).error as UploadEncryptedLogError.Unknown).let { error -> + assertThat(error.statusCode).isEqualTo(500) + assertThat(error.message).isEqualTo("Some other error occurred") + } + } + + @Test + fun `uploadLog returns Unknown error with message for non-JSON error response`() { + mockWebServer.enqueue(MockResponse().setResponseCode(500).setBody("Plain text error message")) + + val result = runBlocking { + restClient.uploadLog(testLogUuid, testContents) + } + + assertThat(((result as UploadEncryptedLogResult.LogUploadFailed).error as UploadEncryptedLogError.Unknown).message) + .isEqualTo("Plain text error message") + } + + @Test + fun `uploadLog returns NoConnection error when IOException occurs`() { + mockWebServer.shutdown() + + val result = runBlocking { + restClient.uploadLog(testLogUuid, testContents) + } + + assertThat((result as UploadEncryptedLogResult.LogUploadFailed).error) + .isInstanceOf(UploadEncryptedLogError.NoConnection::class.java) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 906f3ab..e9e7b67 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ kotlin = "2.1.21" kotlinx-binary-compatibility-validator = "0.18.0" kotlinx-coroutines = '1.10.2' ksp = "2.1.21-2.0.2" +mockwebserver = "3.2.0" robolectric = "4.14.1" terl-lazysodium-android = "5.2.0@aar" turbine = "1.2.1" @@ -35,6 +36,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "mockwebserver" } robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } terl-lazysodium-android = { module = "com.goterl:lazysodium-android", version.ref = "terl-lazysodium-android" } turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" } From c57ff74a3253d3377c8c3649656212fc7095634d Mon Sep 17 00:00:00 2001 From: Wojtek Zieba Date: Thu, 6 Nov 2025 18:40:06 +0100 Subject: [PATCH 3/3] Fix: Include HTTP status code in Unknown error for non-JSON responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the server returns a non-JSON response (e.g., HTTP 500 with HTML), the status code is now included in the UploadEncryptedLogError.Unknown object for consistent error reporting and better debugging. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../rest/wpcom/encryptedlog/EncryptedLogRestClient.kt | 2 +- .../rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt index 1c32207..684788b 100644 --- a/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt +++ b/encryptedlogging/src/main/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClient.kt @@ -36,7 +36,7 @@ internal class EncryptedLogRestClient( JSONObject(responseBody) } catch (jsonException: JSONException) { Log.e(TAG, "Received response not in JSON format: " + jsonException.message) - return UploadEncryptedLogError.Unknown(message = responseBody) + return UploadEncryptedLogError.Unknown(statusCode = statusCode, message = responseBody) } val errorMessage = json.getString("message") json.getString("error").let { errorType -> diff --git a/encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt b/encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt index 23d3bfc..9a743c7 100644 --- a/encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt +++ b/encryptedlogging/src/test/kotlin/com/automattic/encryptedlogging/network/rest/wpcom/encryptedlog/EncryptedLogRestClientTest.kt @@ -123,15 +123,17 @@ class EncryptedLogRestClientTest { } @Test - fun `uploadLog returns Unknown error with message for non-JSON error response`() { + fun `uploadLog returns Unknown error with statusCode and message for non-JSON error response`() { mockWebServer.enqueue(MockResponse().setResponseCode(500).setBody("Plain text error message")) val result = runBlocking { restClient.uploadLog(testLogUuid, testContents) } - assertThat(((result as UploadEncryptedLogResult.LogUploadFailed).error as UploadEncryptedLogError.Unknown).message) - .isEqualTo("Plain text error message") + ((result as UploadEncryptedLogResult.LogUploadFailed).error as UploadEncryptedLogError.Unknown).let { error -> + assertThat(error.statusCode).isEqualTo(500) + assertThat(error.message).isEqualTo("Plain text error message") + } } @Test