Map auth errors to internal representation

This commit is contained in:
Kirill Kamakin
2021-11-21 17:27:22 +03:00
parent e8089c6684
commit 808e1ce359
6 changed files with 92 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
package gq.kirmanak.mealient.data.auth
import gq.kirmanak.mealient.data.auth.impl.GetTokenResponse
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
@@ -15,5 +16,5 @@ interface AuthService {
@Field("scope") scope: String? = null,
@Field("client_id") clientId: String? = null,
@Field("client_secret") clientSecret: String? = null
): GetTokenResponse
): Response<GetTokenResponse>
}

View File

@@ -2,15 +2,25 @@ package gq.kirmanak.mealient.data.auth.impl
import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.auth.AuthService
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.*
import gq.kirmanak.mealient.data.impl.ErrorDetail
import gq.kirmanak.mealient.data.impl.RetrofitBuilder
import kotlinx.coroutines.CancellationException
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import retrofit2.HttpException
import retrofit2.Response
import retrofit2.create
import timber.log.Timber
import java.io.InputStream
import javax.inject.Inject
@ExperimentalSerializationApi
class AuthDataSourceImpl @Inject constructor(
private val retrofitBuilder: RetrofitBuilder
private val retrofitBuilder: RetrofitBuilder,
private val json: Json,
) : AuthDataSource {
override suspend fun authenticate(
username: String,
@@ -19,8 +29,37 @@ class AuthDataSourceImpl @Inject constructor(
): String {
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
val authService = retrofitBuilder.buildRetrofit(baseUrl).create<AuthService>()
val response = authService.getToken(username, password)
Timber.d("authenticate() response is $response")
return response.accessToken
val accessToken = runCatching {
authService.getToken(username, password)
}.mapCatching {
Timber.d("authenticate() response is $it")
if (!it.isSuccessful) {
val cause = HttpException(it)
throw when (it.decodeErrorBodyOrNull<GetTokenResponse, ErrorDetail>()?.detail) {
"Unauthorized" -> Unauthorized(cause)
else -> NotMealie(cause)
}
}
checkNotNull(it.body()).accessToken // Can't be null here, would throw SerializationException
}.onFailure {
Timber.e(it, "authenticate: getToken failed")
throw when (it) {
is CancellationException, is AuthenticationError -> it
is SerializationException, is IllegalStateException -> NotMealie(it)
else -> NoServerConnection(it)
}
}.getOrThrow()
Timber.v("authenticate() returned: $accessToken")
return accessToken
}
private inline fun <reified T, reified R> Response<T>.decodeErrorBodyOrNull(): R? =
errorBody()?.byteStream()?.let { json.decodeFromStreamOrNull<R>(it) }
private inline fun <reified T> Json.decodeFromStreamOrNull(stream: InputStream): T? =
runCatching { decodeFromStream<T>(stream) }
.onFailure { Timber.e(it, "decodeFromStreamOrNull: can't decode") }
.getOrNull()
}

View File

@@ -0,0 +1,7 @@
package gq.kirmanak.mealient.data.auth.impl
sealed class AuthenticationError(cause: Throwable) : RuntimeException(cause) {
class Unauthorized(cause: Throwable) : AuthenticationError(cause)
class NoServerConnection(cause: Throwable) : AuthenticationError(cause)
class NotMealie(cause: Throwable) : AuthenticationError(cause)
}

View File

@@ -0,0 +1,7 @@
package gq.kirmanak.mealient.data.impl
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ErrorDetail(@SerialName("detail") val detail: String? = null)

View File

@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.data.auth.impl
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.*
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
@@ -12,8 +13,8 @@ import gq.kirmanak.mealient.test.MockServerTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import okhttp3.mockwebserver.MockResponse
import org.junit.Test
import retrofit2.HttpException
import javax.inject.Inject
@ExperimentalSerializationApi
@@ -30,7 +31,7 @@ class AuthDataSourceImplTest : MockServerTest() {
assertThat(token).isEqualTo(TEST_TOKEN)
}
@Test(expected = HttpException::class)
@Test(expected = Unauthorized::class)
fun `when authentication isn't successful then throws`(): Unit = runBlocking {
mockServer.enqueueUnsuccessfulAuthResponse()
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
@@ -52,4 +53,32 @@ class AuthDataSourceImplTest : MockServerTest() {
assertThat(path).isEqualTo("/api/auth/token")
}
@Test(expected = NotMealie::class)
fun `when authenticate but response empty then NotMealie`(): Unit = runBlocking {
val response = MockResponse().setResponseCode(200)
mockServer.enqueue(response)
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
}
@Test(expected = NotMealie::class)
fun `when authenticate but response invalid then NotMealie`(): Unit = runBlocking {
val response = MockResponse()
.setResponseCode(200)
.setHeader("Content-Type", "application/json")
.setBody("{\"test\": \"test\"")
mockServer.enqueue(response)
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
}
@Test(expected = NotMealie::class)
fun `when authenticate but response not found then NotMealie`(): Unit = runBlocking {
val response = MockResponse().setResponseCode(404)
mockServer.enqueue(response)
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)
}
@Test(expected = NoServerConnection::class)
fun `when authenticate but host not found then NoServerConnection`(): Unit = runBlocking {
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, "http://test")
}
}

View File

@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.data.auth.impl
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.Unauthorized
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
@@ -11,7 +12,6 @@ import gq.kirmanak.mealient.test.MockServerTest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Test
import retrofit2.HttpException
import javax.inject.Inject
@HiltAndroidTest
@@ -31,7 +31,7 @@ class AuthRepoImplTest : MockServerTest() {
assertThat(subject.authenticationStatuses().first()).isTrue()
}
@Test(expected = HttpException::class)
@Test(expected = Unauthorized::class)
fun `when authentication fails then authenticate throws`() = runBlocking {
mockServer.enqueueUnsuccessfulAuthResponse()
subject.authenticate(TEST_USERNAME, TEST_PASSWORD, serverUrl)