Skip to content

Commit

Permalink
create a method to extract the AuthToken from an auth API response
Browse files Browse the repository at this point in the history
  • Loading branch information
frett committed Nov 3, 2023
1 parent 498c3f4 commit 4b91af3
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 8 deletions.
1 change: 1 addition & 0 deletions library/account/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
ksp(libs.dagger.compiler)
ksp(libs.hilt.compiler)

testImplementation(libs.json)
testImplementation(libs.kotlin.coroutines.test)
testImplementation(libs.turbine)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.Flow
import org.ccci.gto.android.common.Ordered
import org.ccci.gto.android.common.jsonapi.JsonApiConverter
import org.ccci.gto.android.common.jsonapi.model.JsonApiObject
import org.cru.godtools.account.AccountType
import org.cru.godtools.api.model.AuthToken
import org.json.JSONException
import retrofit2.Response

internal interface AccountProvider : Ordered {
val type: AccountType
Expand All @@ -24,3 +28,37 @@ internal interface AccountProvider : Ordered {

suspend fun authenticateWithMobileContentApi(): Result<AuthToken>
}

internal fun Response<JsonApiObject<AuthToken>>.extractAuthToken() = when {
isSuccessful -> {
val obj = body()
val data = obj?.dataSingle

when {
obj == null -> Result.failure(AuthenticationException.UnknownError)
obj.hasErrors -> obj.parseErrors()
data == null -> Result.failure(AuthenticationException.UnknownError)
else -> Result.success(data)
}
}
else -> errorBody()?.string()
?.let {
try {
JsonApiConverter.Builder().build().fromJson(it, AuthToken::class.java)
} catch (_: JSONException) {
null
}
}
?.parseErrors()
?: Result.failure(AuthenticationException.UnknownError)
}

private fun JsonApiObject<AuthToken>.parseErrors(): Result<AuthToken> {
errors.forEach {
when (it.code) {
AuthToken.ERROR_USER_ALREADY_EXISTS -> return Result.failure(AuthenticationException.UserAlreadyExists)
AuthToken.ERROR_USER_NOT_FOUND -> return Result.failure(AuthenticationException.UserNotFound)
}
}
return Result.failure(AuthenticationException.UnknownError)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ internal sealed class AuthenticationException : Exception() {
data object MissingCredentials : AuthenticationException()
data object UnableToRefreshCredentials : AuthenticationException()
data object UnknownError : AuthenticationException()

data object UserAlreadyExists : AuthenticationException()
data object UserNotFound : AuthenticationException()
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import org.ccci.gto.android.common.kotlin.coroutines.getStringFlow
import org.cru.godtools.account.AccountType
import org.cru.godtools.account.provider.AccountProvider
import org.cru.godtools.account.provider.AuthenticationException
import org.cru.godtools.account.provider.extractAuthToken
import org.cru.godtools.api.AuthApi
import org.cru.godtools.api.model.AuthToken

Expand Down Expand Up @@ -80,10 +81,8 @@ internal class FacebookAccountProvider @Inject constructor(
resp = accessToken.authenticateWithMobileContentApi()
}

val token = resp.takeIf { it.isSuccessful }?.body()?.takeUnless { it.hasErrors }?.dataSingle
?: return Result.failure(AuthenticationException.UnknownError)
prefs.edit { putString(accessToken.PREF_USER_ID, token.userId) }
return Result.success(token)
return resp.extractAuthToken()
.onSuccess { prefs.edit { putString(accessToken.PREF_USER_ID, it.userId) } }
}

private suspend fun AccessToken.authenticateWithMobileContentApi() =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.ccci.gto.android.common.play.auth.signin.GoogleSignInKtx
import org.cru.godtools.account.AccountType
import org.cru.godtools.account.provider.AccountProvider
import org.cru.godtools.account.provider.AuthenticationException
import org.cru.godtools.account.provider.extractAuthToken
import org.cru.godtools.api.AuthApi
import org.cru.godtools.api.model.AuthToken
import timber.log.Timber
Expand Down Expand Up @@ -87,10 +88,8 @@ internal class GoogleAccountProvider @Inject constructor(
?: return Result.failure(AuthenticationException.MissingCredentials)
}

val token = resp.takeIf { it.isSuccessful }?.body()?.takeUnless { it.hasErrors }?.dataSingle
?: return Result.failure(AuthenticationException.UnknownError)
prefs.edit { putString(account.PREF_USER_ID, token.userId) }
return Result.success(token)
return resp.extractAuthToken()
.onSuccess { prefs.edit { putString(account.PREF_USER_ID, it.userId) } }
}

private suspend fun GoogleSignInAccount.authenticateWithMobileContentApi() =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.cru.godtools.account.provider

import kotlin.test.Test
import kotlin.test.assertEquals
import okhttp3.ResponseBody.Companion.toResponseBody
import org.ccci.gto.android.common.jsonapi.JsonApiConverter
import org.ccci.gto.android.common.jsonapi.model.JsonApiError
import org.ccci.gto.android.common.jsonapi.model.JsonApiObject
import org.cru.godtools.api.model.AuthToken
import retrofit2.Response

private const val ERROR_USER_ALREADY_EXISTS = """
{"errors":[{"code":"user_already_exists","detail":"User account already exists."}]}
"""
private const val ERROR_USER_NOT_FOUND = """
{"errors":[{"code":"user_not_found","detail":"User account not found."}]}
"""

class AccountProviderTest {
// region extractAuthToken()
@Test
fun `extractAuthToken() - Successful`() {
val token = AuthToken()
val response = Response.success(JsonApiObject.of(token))

assertEquals(token, response.extractAuthToken().getOrNull())
}

@Test
fun `extractAuthToken() - Error - Successful status with jsonapi error`() {
val error = JsonApiError(code = AuthToken.ERROR_USER_ALREADY_EXISTS)
val response = Response.success(JsonApiObject.error<AuthToken>(error))

assertEquals(
AuthenticationException.UserAlreadyExists,
response.extractAuthToken().exceptionOrNull()
)
}

@Test
fun `extractAuthToken() - Error - 500 ISE`() {
val response = Response.error<JsonApiObject<AuthToken>>(500, "500 ISE".toResponseBody())

assertEquals(
AuthenticationException.UnknownError,
response.extractAuthToken().exceptionOrNull()
)
}

@Test
fun `extractAuthToken() - Error - User Already Exists`() {
val response = Response.error<JsonApiObject<AuthToken>>(400, ERROR_USER_ALREADY_EXISTS.toResponseBody())

assertEquals(
AuthenticationException.UserAlreadyExists,
response.extractAuthToken().exceptionOrNull()
)
}

@Test
fun `extractAuthToken() - Error - User Not Found`() {
val response = Response.error<JsonApiObject<AuthToken>>(400, ERROR_USER_NOT_FOUND.toResponseBody())

assertEquals(
AuthenticationException.UserNotFound,
response.extractAuthToken().exceptionOrNull()
)
}

@Test
fun `extractAuthToken() - Error - Valid jsonapi response with error status`() {
val converter = JsonApiConverter.Builder()
.addClasses(AuthToken::class.java)
.build()

val response = Response.error<JsonApiObject<AuthToken>>(
400,
converter.toJson(JsonApiObject.of(AuthToken())).toResponseBody()
)

assertEquals(
AuthenticationException.UnknownError,
response.extractAuthToken().exceptionOrNull()
)
}
// endregion extractAuthToken()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ data class AuthToken(
@JsonApiAttribute(ATTR_TOKEN)
var token: String? = null
) {
companion object {
const val ERROR_USER_ALREADY_EXISTS = "user_already_exists"
const val ERROR_USER_NOT_FOUND = "user_not_found"
}

@JsonApiType("auth-token-request")
data class Request(
@JsonApiAttribute(ATTR_FACEBOOK_ACCESS_TOKEN) val fbAccessToken: String? = null,
Expand Down

0 comments on commit 4b91af3

Please sign in to comment.