mirror of
https://github.com/thunderbird/thunderbird-android.git
synced 2024-09-20 12:12:15 +02:00
Add FinishOAuthSignIn usecase
This commit is contained in:
parent
e658b8256b
commit
615e16895f
@ -23,6 +23,7 @@ dependencies {
|
||||
|
||||
implementation(libs.appauth)
|
||||
implementation(libs.androidx.compose.material)
|
||||
implementation(libs.timber)
|
||||
|
||||
testImplementation(projects.core.ui.compose.testing)
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ package app.k9mail.feature.account.oauth
|
||||
|
||||
import app.k9mail.core.common.coreCommonModule
|
||||
import app.k9mail.feature.account.oauth.data.AuthorizationRepository
|
||||
import app.k9mail.feature.account.oauth.data.AuthorizationStateRepository
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.oauth.domain.usecase.FinishOAuthSignIn
|
||||
import app.k9mail.feature.account.oauth.domain.usecase.GetOAuthRequestIntent
|
||||
import app.k9mail.feature.account.oauth.domain.usecase.SuggestServerName
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthViewModel
|
||||
@ -28,6 +30,10 @@ val featureAccountOAuthModule: Module = module {
|
||||
)
|
||||
}
|
||||
|
||||
factory<DomainContract.AuthorizationStateRepository> {
|
||||
AuthorizationStateRepository()
|
||||
}
|
||||
|
||||
factory<UseCase.SuggestServerName> { SuggestServerName() }
|
||||
|
||||
factory<UseCase.GetOAuthRequestIntent> {
|
||||
@ -37,9 +43,12 @@ val featureAccountOAuthModule: Module = module {
|
||||
)
|
||||
}
|
||||
|
||||
factory<UseCase.FinishOAuthSignIn> { FinishOAuthSignIn(repository = get()) }
|
||||
|
||||
viewModel {
|
||||
AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = get(),
|
||||
finishOAuthSignIn = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
package app.k9mail.feature.account.oauth.data
|
||||
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import net.openid.appauth.AuthState
|
||||
import org.json.JSONException
|
||||
import timber.log.Timber
|
||||
|
||||
fun AuthState.toAuthorizationState(): AuthorizationState {
|
||||
return try {
|
||||
AuthorizationState(state = jsonSerializeString())
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "Error serializing AuthorizationState")
|
||||
AuthorizationState()
|
||||
}
|
||||
}
|
||||
|
||||
fun AuthorizationState.toAuthState(): AuthState {
|
||||
return try {
|
||||
state?.let { AuthState.jsonDeserialize(it) } ?: AuthState()
|
||||
} catch (e: JSONException) {
|
||||
Timber.e(e, "Error deserializing AuthorizationState")
|
||||
AuthState()
|
||||
}
|
||||
}
|
@ -5,10 +5,17 @@ import androidx.core.net.toUri
|
||||
import app.k9mail.core.common.oauth.OAuthConfiguration
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import net.openid.appauth.ResponseTypeValues
|
||||
import timber.log.Timber
|
||||
|
||||
class AuthorizationRepository(
|
||||
private val service: AuthorizationService,
|
||||
@ -23,6 +30,46 @@ class AuthorizationRepository(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getAuthorizationResponse(intent: Intent): AuthorizationResponse? {
|
||||
return try {
|
||||
AuthorizationResponse.fromIntent(intent)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Error deserializing AuthorizationResponse")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getAuthorizationException(intent: Intent): AuthorizationException? {
|
||||
return try {
|
||||
AuthorizationException.fromIntent(intent)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Error deserializing AuthorizationException")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getExchangeToken(
|
||||
authorizationState: AuthorizationState,
|
||||
response: AuthorizationResponse,
|
||||
): AuthorizationResult = suspendCoroutine { continuation ->
|
||||
val tokenRequest = response.createTokenExchangeRequest()
|
||||
val authState = authorizationState.toAuthState()
|
||||
|
||||
service.performTokenRequest(tokenRequest) { tokenResponse, authorizationException ->
|
||||
authState.update(tokenResponse, authorizationException)
|
||||
|
||||
val result = if (authorizationException != null) {
|
||||
AuthorizationResult.Failure(authorizationException)
|
||||
} else if (tokenResponse != null) {
|
||||
AuthorizationResult.Success(authState.toAuthorizationState())
|
||||
} else {
|
||||
AuthorizationResult.Failure(Exception("Unknown error"))
|
||||
}
|
||||
|
||||
continuation.resume(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAuthorizationRequestIntent(configuration: OAuthConfiguration, emailAddress: String): Intent {
|
||||
val serviceConfig = AuthorizationServiceConfiguration(
|
||||
configuration.authorizationEndpoint.toUri(),
|
||||
|
@ -0,0 +1,12 @@
|
||||
package app.k9mail.feature.account.oauth.data
|
||||
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
|
||||
class AuthorizationStateRepository : DomainContract.AuthorizationStateRepository {
|
||||
override suspend fun isAuthorized(authorizationState: AuthorizationState): Boolean {
|
||||
val authState = authorizationState.toAuthState()
|
||||
|
||||
return authState.isAuthorized
|
||||
}
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
package app.k9mail.feature.account.oauth.domain
|
||||
|
||||
import android.content.Intent
|
||||
import app.k9mail.core.common.oauth.OAuthConfiguration
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
|
||||
interface DomainContract {
|
||||
|
||||
@ -13,6 +18,14 @@ interface DomainContract {
|
||||
fun interface GetOAuthRequestIntent {
|
||||
fun execute(hostname: String, emailAddress: String): AuthorizationIntentResult
|
||||
}
|
||||
|
||||
fun interface FinishOAuthSignIn {
|
||||
suspend fun execute(authorizationState: AuthorizationState, intent: Intent): AuthorizationResult
|
||||
}
|
||||
|
||||
fun interface CheckIsAuthorized {
|
||||
suspend fun execute(authorizationState: AuthorizationState): Boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthorizationRepository {
|
||||
@ -20,5 +33,17 @@ interface DomainContract {
|
||||
configuration: OAuthConfiguration,
|
||||
emailAddress: String,
|
||||
): AuthorizationIntentResult
|
||||
|
||||
suspend fun getAuthorizationResponse(intent: Intent): AuthorizationResponse?
|
||||
suspend fun getAuthorizationException(intent: Intent): AuthorizationException?
|
||||
|
||||
suspend fun getExchangeToken(
|
||||
authorizationState: AuthorizationState,
|
||||
response: AuthorizationResponse,
|
||||
): AuthorizationResult
|
||||
}
|
||||
|
||||
interface AuthorizationStateRepository {
|
||||
suspend fun isAuthorized(authorizationState: AuthorizationState): Boolean
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
package app.k9mail.feature.account.oauth.domain.entity
|
||||
|
||||
sealed interface AuthorizationResult {
|
||||
|
||||
data class Success(
|
||||
val state: AuthorizationState,
|
||||
) : AuthorizationResult
|
||||
|
||||
data class Failure(
|
||||
val error: Exception,
|
||||
) : AuthorizationResult
|
||||
|
||||
object BrowserNotAvailable : AuthorizationResult
|
||||
|
||||
object Canceled : AuthorizationResult
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package app.k9mail.feature.account.oauth.domain.usecase
|
||||
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
|
||||
class CheckIsAuthorized(
|
||||
private val repository: DomainContract.AuthorizationStateRepository,
|
||||
) : UseCase.CheckIsAuthorized {
|
||||
override suspend fun execute(authorizationState: AuthorizationState): Boolean {
|
||||
return repository.isAuthorized(authorizationState)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package app.k9mail.feature.account.oauth.domain.usecase
|
||||
|
||||
import android.content.Intent
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
|
||||
class FinishOAuthSignIn(
|
||||
private val repository: DomainContract.AuthorizationRepository,
|
||||
) : UseCase.FinishOAuthSignIn {
|
||||
override suspend fun execute(authorizationState: AuthorizationState, intent: Intent): AuthorizationResult {
|
||||
val response = repository.getAuthorizationResponse(intent)
|
||||
val exception = repository.getAuthorizationException(intent)
|
||||
|
||||
return if (response != null) {
|
||||
repository.getExchangeToken(authorizationState, response)
|
||||
} else if (exception != null) {
|
||||
AuthorizationResult.Failure(exception)
|
||||
} else {
|
||||
AuthorizationResult.Canceled
|
||||
}
|
||||
}
|
||||
}
|
@ -24,6 +24,11 @@ interface AccountOAuthContract {
|
||||
)
|
||||
|
||||
sealed interface Event {
|
||||
data class OnOAuthResult(
|
||||
val resultCode: Int,
|
||||
val data: Intent?,
|
||||
) : Event
|
||||
|
||||
object SignInClicked : Event
|
||||
object OnNextClicked : Event
|
||||
object OnBackClicked : Event
|
||||
@ -43,7 +48,9 @@ interface AccountOAuthContract {
|
||||
|
||||
sealed interface Error {
|
||||
object NotSupported : Error
|
||||
object NetworkError : Error
|
||||
object UnknownError : Error
|
||||
object Cancelled : Error
|
||||
|
||||
object BrowserNotAvailable : Error
|
||||
data class Unknown(val error: Exception) : Error
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@ -27,10 +26,7 @@ fun AccountOAuthScreen(
|
||||
val oAuthLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK && it.data != null) {
|
||||
// TODO handle success
|
||||
}
|
||||
// TODO handle error
|
||||
viewModel.event(Event.OnOAuthResult(it.resultCode, it.data))
|
||||
}
|
||||
|
||||
val (state, dispatch) = viewModel.observe { effect ->
|
||||
|
@ -1,17 +1,23 @@
|
||||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.k9mail.core.ui.compose.common.mvi.BaseViewModel
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase.GetOAuthRequestIntent
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract.UseCase
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Error
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.ViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AccountOAuthViewModel(
|
||||
initialState: State = State(),
|
||||
private val getOAuthRequestIntent: GetOAuthRequestIntent,
|
||||
private val getOAuthRequestIntent: UseCase.GetOAuthRequestIntent,
|
||||
private val finishOAuthSignIn: UseCase.FinishOAuthSignIn,
|
||||
) : BaseViewModel<State, Event, Effect>(initialState), ViewModel {
|
||||
|
||||
override fun initState(state: State) {
|
||||
@ -22,24 +28,19 @@ class AccountOAuthViewModel(
|
||||
|
||||
override fun event(event: Event) {
|
||||
when (event) {
|
||||
Event.SignInClicked -> launchOAuth()
|
||||
is Event.OnOAuthResult -> onOAuthResult(event.resultCode, event.data)
|
||||
|
||||
Event.SignInClicked -> onSignIn()
|
||||
|
||||
Event.OnNextClicked -> TODO()
|
||||
|
||||
Event.OnBackClicked -> navigateBack()
|
||||
|
||||
Event.OnRetryClicked -> {
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
launchOAuth()
|
||||
}
|
||||
Event.OnRetryClicked -> onRetry()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchOAuth() {
|
||||
private fun onSignIn() {
|
||||
val result = getOAuthRequestIntent.execute(
|
||||
hostname = state.value.hostname,
|
||||
emailAddress = state.value.emailAddress,
|
||||
@ -60,5 +61,57 @@ class AccountOAuthViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetry() {
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
onSignIn()
|
||||
}
|
||||
|
||||
private fun onOAuthResult(resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
finishSignIn(data)
|
||||
} else {
|
||||
updateState { state ->
|
||||
state.copy(error = Error.Cancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishSignIn(data: Intent) {
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
isLoading = true,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
when (val result = finishOAuthSignIn.execute(state.value.authorizationState, data)) {
|
||||
AuthorizationResult.BrowserNotAvailable -> updateErrorState(Error.BrowserNotAvailable)
|
||||
AuthorizationResult.Canceled -> updateErrorState(Error.Cancelled)
|
||||
is AuthorizationResult.Failure -> updateErrorState(Error.Unknown(result.error))
|
||||
is AuthorizationResult.Success -> {
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
authorizationState = result.state,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
navigateNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateErrorState(error: Error) = updateState { state ->
|
||||
state.copy(
|
||||
error = error,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateBack() = emitEffect(Effect.NavigateBack)
|
||||
|
||||
private fun navigateNext() = emitEffect(Effect.NavigateNext(state.value.authorizationState))
|
||||
}
|
||||
|
@ -4,9 +4,13 @@ import android.content.Intent
|
||||
import app.k9mail.core.common.oauth.OAuthConfiguration
|
||||
import app.k9mail.feature.account.oauth.domain.DomainContract
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
import org.junit.Test
|
||||
|
||||
class GetOAuthRequestIntentTest {
|
||||
@ -72,5 +76,20 @@ class GetOAuthRequestIntentTest {
|
||||
|
||||
return AuthorizationIntentResult.Success(intent)
|
||||
}
|
||||
|
||||
override suspend fun getAuthorizationResponse(intent: Intent): AuthorizationResponse? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getAuthorizationException(intent: Intent): AuthorizationException? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getExchangeToken(
|
||||
authorizationState: AuthorizationState,
|
||||
response: AuthorizationResponse,
|
||||
): AuthorizationResult {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
package app.k9mail.feature.account.oauth.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import app.cash.turbine.testIn
|
||||
import app.k9mail.core.ui.compose.testing.MainDispatcherRule
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationIntentResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationResult
|
||||
import app.k9mail.feature.account.oauth.domain.entity.AuthorizationState
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Effect
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Error
|
||||
import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.Event
|
||||
@ -11,6 +14,7 @@ import app.k9mail.feature.account.oauth.ui.AccountOAuthContract.State
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.assertThatAndTurbinesConsumed
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -23,26 +27,10 @@ class AccountOAuthViewModelTest {
|
||||
@get:Rule
|
||||
val mainDispatcherRule = MainDispatcherRule()
|
||||
|
||||
private val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.Success(intent = Intent())
|
||||
},
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `should launch OAuth when SignInClicked event received`() = runTest {
|
||||
val initialState = State(
|
||||
hostname = "example.com",
|
||||
emailAddress = "test@example.com",
|
||||
)
|
||||
val intent = Intent()
|
||||
val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.Success(intent = intent)
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
|
||||
val initialState = defaultState
|
||||
val testSubject = createTestSubject(initialState = initialState)
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
@ -66,14 +54,9 @@ class AccountOAuthViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `should show error when SignInClicked event received and OAuth is not supported`() = runTest {
|
||||
val initialState = State(
|
||||
hostname = "example.com",
|
||||
emailAddress = "test@example.com",
|
||||
)
|
||||
val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.NotSupported
|
||||
},
|
||||
val initialState = defaultState
|
||||
val testSubject = createTestSubject(
|
||||
authorizationIntentResult = AuthorizationIntentResult.NotSupported,
|
||||
initialState = initialState,
|
||||
)
|
||||
|
||||
@ -100,19 +83,10 @@ class AccountOAuthViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `should remove error and launch OAuth when OnRetryClicked event received`() = runTest {
|
||||
val initialState = State(
|
||||
hostname = "example.com",
|
||||
emailAddress = "test@example.com",
|
||||
val initialState = defaultState.copy(
|
||||
error = Error.NotSupported,
|
||||
)
|
||||
val intent = Intent()
|
||||
val testSubject = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
AuthorizationIntentResult.Success(intent = intent)
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
|
||||
val testSubject = createTestSubject(initialState = initialState)
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
@ -127,7 +101,7 @@ class AccountOAuthViewModelTest {
|
||||
testSubject.event(Event.OnRetryClicked)
|
||||
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(
|
||||
initialState.copy(error = null)
|
||||
initialState.copy(error = null),
|
||||
)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
@ -138,9 +112,171 @@ class AccountOAuthViewModelTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should finish OAuth sign in when onOAuthResult received with success`() = runTest {
|
||||
val initialState = defaultState
|
||||
val authorizationState = AuthorizationState(state = "state")
|
||||
val testSubject = createTestSubject(
|
||||
authorizationResult = AuthorizationResult.Success(authorizationState),
|
||||
initialState = initialState
|
||||
)
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState)
|
||||
}
|
||||
|
||||
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
|
||||
|
||||
val loadingState = initialState.copy(isLoading = true)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(loadingState)
|
||||
}
|
||||
|
||||
val successState = loadingState.copy(
|
||||
isLoading = false,
|
||||
authorizationState = authorizationState,
|
||||
)
|
||||
|
||||
assertThat(stateTurbine.awaitItem()).isEqualTo(successState)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = effectTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(Effect.NavigateNext(authorizationState))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set error state when onOAuthResult received with canceled`() = runTest {
|
||||
val initialState = defaultState
|
||||
val testSubject = createTestSubject(
|
||||
authorizationResult = AuthorizationResult.Canceled,
|
||||
initialState = initialState
|
||||
)
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState)
|
||||
}
|
||||
|
||||
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_CANCELED, data = intent))
|
||||
|
||||
val failureState = initialState.copy(
|
||||
error = Error.Cancelled,
|
||||
)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(failureState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is cancelled`() = runTest {
|
||||
val initialState = defaultState
|
||||
val testSubject = createTestSubject(
|
||||
authorizationResult = AuthorizationResult.Canceled,
|
||||
initialState = initialState
|
||||
)
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState)
|
||||
}
|
||||
|
||||
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
|
||||
|
||||
val loadingState = initialState.copy(isLoading = true)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(loadingState)
|
||||
}
|
||||
|
||||
val failureState = loadingState.copy(
|
||||
isLoading = false,
|
||||
error = Error.Cancelled,
|
||||
)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(failureState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should finish OAuth sign in when onOAuthResult received with success but authorization result is failure`() = runTest {
|
||||
val initialState = defaultState
|
||||
val failure = Exception("failure")
|
||||
val testSubject = createTestSubject(
|
||||
authorizationResult = AuthorizationResult.Failure(failure),
|
||||
initialState = initialState
|
||||
)
|
||||
val stateTurbine = testSubject.state.testIn(backgroundScope)
|
||||
val effectTurbine = testSubject.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(initialState)
|
||||
}
|
||||
|
||||
testSubject.event(Event.OnOAuthResult(resultCode = Activity.RESULT_OK, data = intent))
|
||||
|
||||
val loadingState = initialState.copy(isLoading = true)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(loadingState)
|
||||
}
|
||||
|
||||
val failureState = loadingState.copy(
|
||||
isLoading = false,
|
||||
error = Error.Unknown(failure),
|
||||
)
|
||||
|
||||
assertThatAndTurbinesConsumed(
|
||||
actual = stateTurbine.awaitItem(),
|
||||
turbines = turbines,
|
||||
) {
|
||||
isEqualTo(failureState)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should emit NavigateBack effect when OnBackClicked event received`() = runTest {
|
||||
val viewModel = testSubject
|
||||
val viewModel = createTestSubject()
|
||||
val stateTurbine = viewModel.state.testIn(backgroundScope)
|
||||
val effectTurbine = viewModel.effect.testIn(backgroundScope)
|
||||
val turbines = listOf(stateTurbine, effectTurbine)
|
||||
@ -161,4 +297,32 @@ class AccountOAuthViewModelTest {
|
||||
isEqualTo(Effect.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val defaultState = State(
|
||||
hostname = "example.com",
|
||||
emailAddress = "test@example.com",
|
||||
)
|
||||
|
||||
val intent = Intent()
|
||||
|
||||
fun createTestSubject(
|
||||
authorizationIntentResult: AuthorizationIntentResult = AuthorizationIntentResult.Success(intent = intent),
|
||||
authorizationResult: AuthorizationResult = AuthorizationResult.Success(AuthorizationState()),
|
||||
isGoogleSignIn: Boolean = false,
|
||||
initialState: State = State(),
|
||||
) = AccountOAuthViewModel(
|
||||
getOAuthRequestIntent = { _, _ ->
|
||||
authorizationIntentResult
|
||||
},
|
||||
finishOAuthSignIn = { _, _ ->
|
||||
delay(50)
|
||||
authorizationResult
|
||||
},
|
||||
checkIsGoogleSignIn = { _ ->
|
||||
isGoogleSignIn
|
||||
},
|
||||
initialState = initialState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user