feat: separate session and credential actions
This commit is contained in:
@@ -52,6 +52,7 @@ fun ABoxManagerApp(
|
|||||||
val actionState = sessionActionsViewModel.uiState.collectAsStateWithLifecycle().value
|
val actionState = sessionActionsViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
val sessionState = appSessionViewModel.uiState.collectAsStateWithLifecycle().value
|
val sessionState = appSessionViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
val settingsState = settingsViewModel.uiState.collectAsStateWithLifecycle().value
|
val settingsState = settingsViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
|
val actionError = actionState.errorMessage?.asString()
|
||||||
val screenKey = sessionState.screenKey()
|
val screenKey = sessionState.screenKey()
|
||||||
LaunchedEffect(screenKey) {
|
LaunchedEffect(screenKey) {
|
||||||
sessionActionsViewModel.dismissError()
|
sessionActionsViewModel.dismissError()
|
||||||
@@ -74,7 +75,7 @@ fun ABoxManagerApp(
|
|||||||
is AppSessionUiState.Login -> LoginRoute(
|
is AppSessionUiState.Login -> LoginRoute(
|
||||||
profile = state.profile,
|
profile = state.profile,
|
||||||
bootstrapError = state.bootstrapError,
|
bootstrapError = state.bootstrapError,
|
||||||
actionError = actionState.errorMessage,
|
actionError = actionError,
|
||||||
actionInProgress = actionState.inProgress,
|
actionInProgress = actionState.inProgress,
|
||||||
onSwitchProvider = sessionActionsViewModel::switchProvider,
|
onSwitchProvider = sessionActionsViewModel::switchProvider,
|
||||||
viewModel = loginViewModel,
|
viewModel = loginViewModel,
|
||||||
@@ -83,7 +84,7 @@ fun ABoxManagerApp(
|
|||||||
username = state.username,
|
username = state.username,
|
||||||
profile = state.profile,
|
profile = state.profile,
|
||||||
actionInProgress = actionState.inProgress,
|
actionInProgress = actionState.inProgress,
|
||||||
errorMessage = actionState.errorMessage,
|
errorMessage = actionError,
|
||||||
onLogout = sessionActionsViewModel::logout,
|
onLogout = sessionActionsViewModel::logout,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-8
@@ -13,13 +13,6 @@ object DomainErrorMapper {
|
|||||||
is DomainError.InvalidRequest -> UiText.StringResource(R.string.error_invalid_request)
|
is DomainError.InvalidRequest -> UiText.StringResource(R.string.error_invalid_request)
|
||||||
is DomainError.OperationFailed -> UiText.StringResource(R.string.error_operation_failed)
|
is DomainError.OperationFailed -> UiText.StringResource(R.string.error_operation_failed)
|
||||||
is DomainError.NoData -> UiText.StringResource(R.string.error_no_data)
|
is DomainError.NoData -> UiText.StringResource(R.string.error_no_data)
|
||||||
is DomainError.Unknown -> {
|
is DomainError.Unknown -> UiText.StringResource(R.string.error_unknown)
|
||||||
val detail = error.detail
|
|
||||||
if (!detail.isNullOrBlank()) {
|
|
||||||
UiText.DynamicString(detail)
|
|
||||||
} else {
|
|
||||||
UiText.StringResource(R.string.error_unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-3
@@ -4,6 +4,9 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.flagship.abox.manager.domain.DomainResult
|
import com.flagship.abox.manager.domain.DomainResult
|
||||||
import com.flagship.abox.manager.domain.SessionCoordinator
|
import com.flagship.abox.manager.domain.SessionCoordinator
|
||||||
|
import com.flagship.abox.manager.domain.SessionValidity
|
||||||
|
import com.flagship.abox.manager.ui.common.DomainErrorMapper
|
||||||
|
import com.flagship.abox.manager.ui.common.UiText
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -18,9 +21,21 @@ class SessionActionsViewModel @Inject constructor(
|
|||||||
private val _uiState = MutableStateFlow(SessionActionsUiState())
|
private val _uiState = MutableStateFlow(SessionActionsUiState())
|
||||||
val uiState: StateFlow<SessionActionsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SessionActionsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
fun onAction(action: SessionUiAction) {
|
||||||
|
when (action) {
|
||||||
|
SessionUiAction.EndSession -> runAction(coordinator::endActiveSession)
|
||||||
|
SessionUiAction.ForgetSavedCredentials -> runAction(coordinator::forgetSavedCredentials)
|
||||||
|
SessionUiAction.SwitchProvider -> runAction(coordinator::switchProvider)
|
||||||
|
SessionUiAction.ValidateConnection -> runValidation()
|
||||||
|
SessionUiAction.DismissError -> dismissError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispatch(action: SessionUiAction) = onAction(action)
|
||||||
|
|
||||||
fun logout() = runAction(coordinator::logout)
|
fun logout() = runAction(coordinator::logout)
|
||||||
|
|
||||||
fun switchProvider() = runAction(coordinator::switchProvider)
|
fun switchProvider() = onAction(SessionUiAction.SwitchProvider)
|
||||||
|
|
||||||
fun dismissError() {
|
fun dismissError() {
|
||||||
_uiState.value = _uiState.value.copy(errorMessage = null)
|
_uiState.value = _uiState.value.copy(errorMessage = null)
|
||||||
@@ -32,7 +47,19 @@ class SessionActionsViewModel @Inject constructor(
|
|||||||
_uiState.value = SessionActionsUiState(inProgress = true)
|
_uiState.value = SessionActionsUiState(inProgress = true)
|
||||||
val result = action()
|
val result = action()
|
||||||
_uiState.value = SessionActionsUiState(
|
_uiState.value = SessionActionsUiState(
|
||||||
errorMessage = (result as? DomainResult.Failure)?.error?.detail,
|
errorMessage = (result as? DomainResult.Failure)?.error?.let(DomainErrorMapper::map),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runValidation() {
|
||||||
|
if (_uiState.value.inProgress) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.value = SessionActionsUiState(inProgress = true)
|
||||||
|
val result = coordinator.validateActiveSession()
|
||||||
|
_uiState.value = SessionActionsUiState(
|
||||||
|
validity = (result as? DomainResult.Success)?.value,
|
||||||
|
errorMessage = (result as? DomainResult.Failure)?.error?.let(DomainErrorMapper::map),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,5 +67,14 @@ class SessionActionsViewModel @Inject constructor(
|
|||||||
|
|
||||||
data class SessionActionsUiState(
|
data class SessionActionsUiState(
|
||||||
val inProgress: Boolean = false,
|
val inProgress: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val validity: SessionValidity? = null,
|
||||||
|
val errorMessage: UiText? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed interface SessionUiAction {
|
||||||
|
data object EndSession : SessionUiAction
|
||||||
|
data object ForgetSavedCredentials : SessionUiAction
|
||||||
|
data object SwitchProvider : SessionUiAction
|
||||||
|
data object ValidateConnection : SessionUiAction
|
||||||
|
data object DismissError : SessionUiAction
|
||||||
|
}
|
||||||
|
|||||||
+197
@@ -0,0 +1,197 @@
|
|||||||
|
package com.flagship.abox.manager.ui.session
|
||||||
|
|
||||||
|
import com.flagship.abox.manager.R
|
||||||
|
import com.flagship.abox.manager.domain.DomainError
|
||||||
|
import com.flagship.abox.manager.domain.LoginCredentials
|
||||||
|
import com.flagship.abox.manager.domain.Password
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfile
|
||||||
|
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||||
|
import com.flagship.abox.manager.domain.ProviderType
|
||||||
|
import com.flagship.abox.manager.domain.SessionCoordinator
|
||||||
|
import com.flagship.abox.manager.domain.SessionValidity
|
||||||
|
import com.flagship.abox.manager.domain.Username
|
||||||
|
import com.flagship.abox.manager.testing.MainDispatcherRule
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeCredentialStore
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeLocalSessionGateway
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeProviderProfileStore
|
||||||
|
import com.flagship.abox.manager.testing.fakes.FakeSessionGateway
|
||||||
|
import com.flagship.abox.manager.ui.common.UiText
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
class SessionActionsViewModelTest {
|
||||||
|
@get:Rule
|
||||||
|
val mainDispatcherRule = MainDispatcherRule()
|
||||||
|
|
||||||
|
private val profile = ProviderProfile(
|
||||||
|
id = ProviderProfileId("abox"),
|
||||||
|
type = ProviderType.ABOX,
|
||||||
|
displayName = "ABox",
|
||||||
|
)
|
||||||
|
private val credentials = LoginCredentials(Username("demo"), Password("secret"))
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun endSessionActionEndsSessionWithoutForgettingCredentials() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.activateAuthenticatedProfile()
|
||||||
|
fixture.coordinator.login(profile.id, credentials, rememberUsername = true, autoLogin = true)
|
||||||
|
val viewModel = SessionActionsViewModel(fixture.coordinator)
|
||||||
|
|
||||||
|
viewModel.onAction(SessionUiAction.EndSession)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertTrue(fixture.remote.session(profile.id) == null)
|
||||||
|
assertEquals(
|
||||||
|
credentials,
|
||||||
|
fixture.credentials.loadCredentials(profile.id).successValue(),
|
||||||
|
)
|
||||||
|
assertNull(viewModel.uiState.value.errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun forgetCredentialsActionKeepsSessionAndDisablesAutoLogin() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.activateAuthenticatedProfile()
|
||||||
|
fixture.coordinator.login(profile.id, credentials, rememberUsername = true, autoLogin = true)
|
||||||
|
val viewModel = SessionActionsViewModel(fixture.coordinator)
|
||||||
|
|
||||||
|
viewModel.onAction(SessionUiAction.ForgetSavedCredentials)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(profile.id, fixture.remote.session(profile.id)?.profileId)
|
||||||
|
assertNull(fixture.credentials.loadCredentials(profile.id).successValue())
|
||||||
|
assertTrue(!fixture.profiles.loginPreferences(profile.id).autoLoginEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun switchProviderActionClearsProfileOnlyAfterLogout() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.activateAuthenticatedProfile()
|
||||||
|
fixture.remote.login(profile.id, credentials)
|
||||||
|
val viewModel = SessionActionsViewModel(fixture.coordinator)
|
||||||
|
|
||||||
|
viewModel.onAction(SessionUiAction.SwitchProvider)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(profile.id, fixture.remote.logoutProfileId)
|
||||||
|
assertNull(fixture.profiles.activeProfile())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateConnectionActionDelegatesWithoutRelogin() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.activateAuthenticatedProfile()
|
||||||
|
fixture.remote.setValidationResult(
|
||||||
|
profile.id,
|
||||||
|
com.flagship.abox.manager.domain.DomainResult.Success(SessionValidity.INVALID),
|
||||||
|
)
|
||||||
|
val viewModel = SessionActionsViewModel(fixture.coordinator)
|
||||||
|
|
||||||
|
viewModel.onAction(SessionUiAction.ValidateConnection)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(SessionValidity.INVALID, fixture.remote.lastValidation)
|
||||||
|
assertEquals(0, fixture.remote.loginCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mappedActionErrorNeverExposesDomainDetail() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.activateAuthenticatedProfile()
|
||||||
|
fixture.remote.logoutError = DomainError.Unknown(detail = "keystore secret detail")
|
||||||
|
val viewModel = SessionActionsViewModel(fixture.coordinator)
|
||||||
|
|
||||||
|
viewModel.dispatch(SessionUiAction.EndSession)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
UiText.StringResource(R.string.error_unknown),
|
||||||
|
viewModel.uiState.value.errorMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dismissErrorActionClearsMappedError() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.activateAuthenticatedProfile()
|
||||||
|
fixture.remote.logoutError = DomainError.Network(detail = "private network detail")
|
||||||
|
val viewModel = SessionActionsViewModel(fixture.coordinator)
|
||||||
|
|
||||||
|
viewModel.onAction(SessionUiAction.EndSession)
|
||||||
|
advanceUntilIdle()
|
||||||
|
viewModel.onAction(SessionUiAction.DismissError)
|
||||||
|
|
||||||
|
assertNull(viewModel.uiState.value.errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Fixture {
|
||||||
|
val profiles = FakeProviderProfileStore()
|
||||||
|
val credentials = FakeCredentialStore()
|
||||||
|
val remote = RecordingSessionGateway(profiles)
|
||||||
|
val local = FakeLocalSessionGateway()
|
||||||
|
val coordinator = SessionCoordinator(profiles, credentials, remote, local)
|
||||||
|
|
||||||
|
suspend fun activateAuthenticatedProfile() {
|
||||||
|
profiles.saveProfile(
|
||||||
|
ProviderProfile(
|
||||||
|
id = ProviderProfileId("abox"),
|
||||||
|
type = ProviderType.ABOX,
|
||||||
|
displayName = "ABox",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
profiles.setActiveProfile(ProviderProfileId("abox"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RecordingSessionGateway(
|
||||||
|
private val profiles: FakeProviderProfileStore,
|
||||||
|
private val delegate: FakeSessionGateway = FakeSessionGateway(),
|
||||||
|
) : com.flagship.abox.manager.domain.SessionGateway by delegate {
|
||||||
|
var loginCalls = 0
|
||||||
|
private set
|
||||||
|
var logoutProfileId: ProviderProfileId? = null
|
||||||
|
private set
|
||||||
|
var lastValidation: SessionValidity? = null
|
||||||
|
private set
|
||||||
|
var logoutError: DomainError? = null
|
||||||
|
|
||||||
|
override suspend fun login(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
credentials: LoginCredentials,
|
||||||
|
) = delegate.login(profileId, credentials).also { loginCalls++ }
|
||||||
|
|
||||||
|
override suspend fun validateSession(profileId: ProviderProfileId) = delegate
|
||||||
|
.validateSession(profileId)
|
||||||
|
.also { result ->
|
||||||
|
lastValidation = (result as? com.flagship.abox.manager.domain.DomainResult.Success)?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun logout(profileId: ProviderProfileId) = logoutError?.let {
|
||||||
|
logoutProfileId = profiles.activeProfile()?.id
|
||||||
|
com.flagship.abox.manager.domain.DomainResult.Failure(it)
|
||||||
|
} ?: delegate.logout(profileId).also {
|
||||||
|
logoutProfileId = profiles.activeProfile()?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun session(profileId: ProviderProfileId) = delegate.observeSession(profileId)
|
||||||
|
.let { flow -> flow.first() }
|
||||||
|
|
||||||
|
fun setValidationResult(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
result: com.flagship.abox.manager.domain.DomainResult<SessionValidity>,
|
||||||
|
) {
|
||||||
|
delegate.setValidationResult(profileId, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> com.flagship.abox.manager.domain.DomainResult<T>.successValue(): T? =
|
||||||
|
(this as? com.flagship.abox.manager.domain.DomainResult.Success)?.value
|
||||||
@@ -147,6 +147,35 @@ class SessionCoordinator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun endActiveSession(): DomainResult<Unit> = storeOperation {
|
||||||
|
val profile = profileStore.activeProfile() ?: return@storeOperation DomainResult.Success(Unit)
|
||||||
|
when (val logoutResult = profile.gateway().logout(profile.id)) {
|
||||||
|
is DomainResult.Failure -> return@storeOperation logoutResult
|
||||||
|
is DomainResult.Success -> Unit
|
||||||
|
}
|
||||||
|
if (profile.type == ProviderType.LOCAL_EXPERIENCE) {
|
||||||
|
profileStore.setActiveProfile(null)
|
||||||
|
} else {
|
||||||
|
DomainResult.Success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun forgetSavedCredentials(): DomainResult<Unit> = storeOperation {
|
||||||
|
val profile = profileStore.activeProfile() ?: return@storeOperation DomainResult.Success(Unit)
|
||||||
|
when (val credentialResult = credentialStore.clearCredentials(profile.id)) {
|
||||||
|
is DomainResult.Failure -> return@storeOperation credentialResult
|
||||||
|
is DomainResult.Success -> Unit
|
||||||
|
}
|
||||||
|
val preferences = profileStore.loginPreferences(profile.id)
|
||||||
|
profileStore.saveLoginPreferences(preferences.copy(autoLoginEnabled = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun validateActiveSession(): DomainResult<SessionValidity> = storeOperation {
|
||||||
|
val profile = profileStore.activeProfile()
|
||||||
|
?: return@storeOperation DomainResult.Success(SessionValidity.INVALID)
|
||||||
|
profile.gateway().validateSession(profile.id)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun logout(): DomainResult<Unit> = storeOperation {
|
suspend fun logout(): DomainResult<Unit> = storeOperation {
|
||||||
val profile = profileStore.activeProfile() ?: return@storeOperation DomainResult.Success(Unit)
|
val profile = profileStore.activeProfile() ?: return@storeOperation DomainResult.Success(Unit)
|
||||||
when (val credentialResult = credentialStore.clearCredentials(profile.id)) {
|
when (val credentialResult = credentialStore.clearCredentials(profile.id)) {
|
||||||
|
|||||||
+128
@@ -12,6 +12,7 @@ import com.flagship.abox.manager.domain.ProviderType
|
|||||||
import com.flagship.abox.manager.domain.SessionCoordinator
|
import com.flagship.abox.manager.domain.SessionCoordinator
|
||||||
import com.flagship.abox.manager.domain.SessionGateway
|
import com.flagship.abox.manager.domain.SessionGateway
|
||||||
import com.flagship.abox.manager.domain.Session
|
import com.flagship.abox.manager.domain.Session
|
||||||
|
import com.flagship.abox.manager.domain.SessionValidity
|
||||||
import com.flagship.abox.manager.domain.Username
|
import com.flagship.abox.manager.domain.Username
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -133,6 +134,101 @@ class SessionCoordinatorTest {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun endActiveSessionKeepsAboxCredentials() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.coordinator.selectProvider(remoteProfile)
|
||||||
|
fixture.coordinator.login(
|
||||||
|
remoteProfile.id,
|
||||||
|
credentials,
|
||||||
|
rememberUsername = true,
|
||||||
|
autoLogin = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = fixture.coordinator.endActiveSession()
|
||||||
|
|
||||||
|
assertTrue(result is DomainResult.Success)
|
||||||
|
assertEquals(remoteProfile, fixture.profiles.activeProfile())
|
||||||
|
assertNull(fixture.remote.observeSession(remoteProfile.id).first())
|
||||||
|
assertEquals(
|
||||||
|
credentials,
|
||||||
|
(fixture.credentials.loadCredentials(remoteProfile.id) as DomainResult.Success).value,
|
||||||
|
)
|
||||||
|
assertTrue(fixture.profiles.loginPreferences(remoteProfile.id).autoLoginEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun forgetSavedCredentialsDisablesAutoLoginAndKeepsSessionStateExplicit() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.coordinator.selectProvider(remoteProfile)
|
||||||
|
fixture.coordinator.login(
|
||||||
|
remoteProfile.id,
|
||||||
|
credentials,
|
||||||
|
rememberUsername = true,
|
||||||
|
autoLogin = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = fixture.coordinator.forgetSavedCredentials()
|
||||||
|
|
||||||
|
assertTrue(result is DomainResult.Success)
|
||||||
|
assertEquals(remoteProfile, fixture.profiles.activeProfile())
|
||||||
|
assertEquals(
|
||||||
|
remoteProfile.id,
|
||||||
|
fixture.remote.observeSession(remoteProfile.id).first()?.profileId,
|
||||||
|
)
|
||||||
|
assertNull(
|
||||||
|
(fixture.credentials.loadCredentials(remoteProfile.id) as DomainResult.Success).value,
|
||||||
|
)
|
||||||
|
val preferences = fixture.profiles.loginPreferences(remoteProfile.id)
|
||||||
|
assertEquals(credentials.username, preferences.rememberedUsername)
|
||||||
|
assertTrue(!preferences.autoLoginEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun switchProviderClearsActiveProfileAfterGatewayLogout() = runTest {
|
||||||
|
val profiles = FakeProviderProfileStore()
|
||||||
|
val remote = RecordingSessionGateway(profiles)
|
||||||
|
val coordinator = SessionCoordinator(
|
||||||
|
profileStore = profiles,
|
||||||
|
credentialStore = FakeCredentialStore(),
|
||||||
|
remoteGateway = remote,
|
||||||
|
localGateway = FakeLocalSessionGateway(),
|
||||||
|
)
|
||||||
|
coordinator.selectProvider(remoteProfile)
|
||||||
|
remote.login(remoteProfile.id, credentials)
|
||||||
|
|
||||||
|
val result = coordinator.switchProvider()
|
||||||
|
|
||||||
|
assertTrue(result is DomainResult.Success)
|
||||||
|
assertEquals(remoteProfile.id, remote.activeProfileAtLogout)
|
||||||
|
assertNull(profiles.activeProfile())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateActiveSessionDoesNotRelogin() = runTest {
|
||||||
|
val remote = CountingSessionGateway()
|
||||||
|
val credentialsStore = CountingCredentialStore()
|
||||||
|
val profiles = FakeProviderProfileStore()
|
||||||
|
val coordinator = SessionCoordinator(
|
||||||
|
profileStore = profiles,
|
||||||
|
credentialStore = credentialsStore,
|
||||||
|
remoteGateway = remote,
|
||||||
|
localGateway = FakeLocalSessionGateway(),
|
||||||
|
)
|
||||||
|
profiles.saveProfile(remoteProfile)
|
||||||
|
profiles.setActiveProfile(remoteProfile.id)
|
||||||
|
remote.setValidationResult(
|
||||||
|
remoteProfile.id,
|
||||||
|
DomainResult.Success(SessionValidity.INVALID),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = coordinator.validateActiveSession()
|
||||||
|
|
||||||
|
assertEquals(DomainResult.Success(SessionValidity.INVALID), result)
|
||||||
|
assertEquals(0, remote.loginCalls)
|
||||||
|
assertEquals(0, credentialsStore.loadCalls)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun authenticationFailureDuringRecoveryClearsSavedCredential() = runTest {
|
fun authenticationFailureDuringRecoveryClearsSavedCredential() = runTest {
|
||||||
val fixture = Fixture()
|
val fixture = Fixture()
|
||||||
@@ -189,6 +285,18 @@ class SessionCoordinatorTest {
|
|||||||
assertTrue(!fixture.profiles.loginPreferences(remoteProfile.id).autoLoginEnabled)
|
assertTrue(!fixture.profiles.loginPreferences(remoteProfile.id).autoLoginEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun endActiveSessionClearsLocalActiveProfile() = runTest {
|
||||||
|
val fixture = Fixture()
|
||||||
|
fixture.coordinator.selectProvider(localProfile)
|
||||||
|
|
||||||
|
val result = fixture.coordinator.endActiveSession()
|
||||||
|
|
||||||
|
assertTrue(result is DomainResult.Success)
|
||||||
|
assertNull(fixture.profiles.activeProfile())
|
||||||
|
assertNull(fixture.local.observeSession(localProfile.id).first())
|
||||||
|
}
|
||||||
|
|
||||||
private class Fixture {
|
private class Fixture {
|
||||||
val profiles = FakeProviderProfileStore()
|
val profiles = FakeProviderProfileStore()
|
||||||
val credentials = FakeCredentialStore()
|
val credentials = FakeCredentialStore()
|
||||||
@@ -197,6 +305,19 @@ class SessionCoordinatorTest {
|
|||||||
val coordinator = SessionCoordinator(profiles, credentials, remote, local)
|
val coordinator = SessionCoordinator(profiles, credentials, remote, local)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class RecordingSessionGateway(
|
||||||
|
private val profileStore: FakeProviderProfileStore,
|
||||||
|
private val delegate: FakeSessionGateway = FakeSessionGateway(),
|
||||||
|
) : SessionGateway by delegate {
|
||||||
|
var activeProfileAtLogout: ProviderProfileId? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
override suspend fun logout(profileId: ProviderProfileId): DomainResult<Unit> {
|
||||||
|
activeProfileAtLogout = profileStore.activeProfile()?.id
|
||||||
|
return delegate.logout(profileId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class CountingSessionGateway(
|
private class CountingSessionGateway(
|
||||||
private val delegate: FakeSessionGateway = FakeSessionGateway(),
|
private val delegate: FakeSessionGateway = FakeSessionGateway(),
|
||||||
) : SessionGateway by delegate {
|
) : SessionGateway by delegate {
|
||||||
@@ -214,6 +335,13 @@ class SessionCoordinatorTest {
|
|||||||
fun failLogin(profileId: ProviderProfileId, error: DomainError) {
|
fun failLogin(profileId: ProviderProfileId, error: DomainError) {
|
||||||
delegate.failLogin(profileId, error)
|
delegate.failLogin(profileId, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setValidationResult(
|
||||||
|
profileId: ProviderProfileId,
|
||||||
|
result: DomainResult<SessionValidity>,
|
||||||
|
) {
|
||||||
|
delegate.setValidationResult(profileId, result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CountingCredentialStore(
|
private class CountingCredentialStore(
|
||||||
|
|||||||
Reference in New Issue
Block a user