mirror of
https://github.com/bitwarden/android.git
synced 2026-05-04 11:05:13 -05:00
🍒[PM-33394] fix: Propagate CookieRedirectException error message (#6640)
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
@@ -45,8 +48,26 @@ sealed class BitwardenError {
|
||||
*/
|
||||
fun Throwable.toBitwardenError(): BitwardenError {
|
||||
return when (this) {
|
||||
// CookieRedirectException is a subclass of IOException thrown when SSO cookies
|
||||
// expire in a load-balanced environment. It must be checked before IOException to
|
||||
// avoid being classified as a generic Network error. We synthesize an Http error
|
||||
// with a JSON body so the exception's message propagates through the existing
|
||||
// parseErrorBodyOrNull pipeline used by service-layer recoverCatching blocks.
|
||||
is CookieRedirectException -> {
|
||||
BitwardenError.Http(
|
||||
throwable = HttpException(
|
||||
Response.error<Any>(
|
||||
HTTP_CODE_BAD_REQUEST,
|
||||
"""{"message": "${this.message}"}""".toResponseBody(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is IOException -> BitwardenError.Network(this)
|
||||
is HttpException -> BitwardenError.Http(this)
|
||||
else -> BitwardenError.Other(this)
|
||||
}
|
||||
}
|
||||
|
||||
private const val HTTP_CODE_BAD_REQUEST: Int = 400
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
class BitwardenErrorTest {
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with CookieRedirectException should return Http with status 400`() {
|
||||
val exception = CookieRedirectException(hostname = "example.com")
|
||||
|
||||
val result = exception.toBitwardenError()
|
||||
|
||||
assertTrue(result is BitwardenError.Http)
|
||||
val httpError = result as BitwardenError.Http
|
||||
assertEquals(400, httpError.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with CookieRedirectException should include message in body`() {
|
||||
val exception = CookieRedirectException(hostname = "example.com")
|
||||
|
||||
val result = exception.toBitwardenError()
|
||||
|
||||
val httpError = result as BitwardenError.Http
|
||||
val body = httpError.responseBodyString
|
||||
assertTrue(body?.contains(exception.message.orEmpty()) == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with IOException should return Network`() {
|
||||
val exception = IOException("network failure")
|
||||
|
||||
val result = exception.toBitwardenError()
|
||||
|
||||
assertTrue(result is BitwardenError.Network)
|
||||
assertEquals(exception, result.throwable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with HttpException should return Http`() {
|
||||
val exception = HttpException(
|
||||
Response.error<Unit>(400, "error".toResponseBody()),
|
||||
)
|
||||
|
||||
val result = exception.toBitwardenError()
|
||||
|
||||
assertTrue(result is BitwardenError.Http)
|
||||
assertEquals(exception, result.throwable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toBitwardenError with RuntimeException should return Other`() {
|
||||
val exception = RuntimeException("unexpected")
|
||||
|
||||
val result = exception.toBitwardenError()
|
||||
|
||||
assertTrue(result is BitwardenError.Other)
|
||||
assertEquals(exception, result.throwable)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.bitwarden.network.util
|
||||
|
||||
import com.bitwarden.network.exception.CookieRedirectException
|
||||
import com.bitwarden.network.model.BitwardenError
|
||||
import com.bitwarden.network.model.CreateCipherResponseJson
|
||||
import com.bitwarden.network.model.toBitwardenError
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
class ExceptionExtensionsTest {
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseErrorBodyOrNull with CookieRedirectException should extract message`() {
|
||||
val expectedMessage = "Your request was interrupted because the app " +
|
||||
"needed to re-authenticate. Please try again."
|
||||
val error = CookieRedirectException(hostname = "example.com")
|
||||
.toBitwardenError()
|
||||
|
||||
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
|
||||
codes = listOf(NetworkErrorCode.BAD_REQUEST),
|
||||
json = json,
|
||||
)
|
||||
|
||||
assertEquals(expectedMessage, result?.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseErrorBodyOrNull with Http and matching code should parse body`() {
|
||||
val responseBody = """
|
||||
{
|
||||
"message": "Bad request",
|
||||
"validationErrors": {
|
||||
"Name": ["Name is required"]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val error = BitwardenError.Http(
|
||||
throwable = HttpException(
|
||||
Response.error<Unit>(400, responseBody.toResponseBody()),
|
||||
),
|
||||
)
|
||||
|
||||
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
|
||||
codes = listOf(NetworkErrorCode.BAD_REQUEST),
|
||||
json = json,
|
||||
)
|
||||
|
||||
assertEquals("Bad request", result?.message)
|
||||
assertEquals(
|
||||
mapOf("Name" to listOf("Name is required")),
|
||||
result?.validationErrors,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseErrorBodyOrNull with Http and non-matching code should return null`() {
|
||||
val responseBody = """
|
||||
{
|
||||
"message": "Bad request",
|
||||
"validationErrors": null
|
||||
}
|
||||
""".trimIndent()
|
||||
val error = BitwardenError.Http(
|
||||
throwable = HttpException(
|
||||
Response.error<Unit>(400, responseBody.toResponseBody()),
|
||||
),
|
||||
)
|
||||
|
||||
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
|
||||
codes = listOf(NetworkErrorCode.UNAUTHORIZED),
|
||||
json = json,
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseErrorBodyOrNull with Network should return null`() {
|
||||
val error = BitwardenError.Network(throwable = IOException("timeout"))
|
||||
|
||||
val result = error.parseErrorBodyOrNull<CreateCipherResponseJson.Invalid>(
|
||||
codes = listOf(NetworkErrorCode.BAD_REQUEST),
|
||||
json = json,
|
||||
)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user