feat: separate session and credential actions
This commit is contained in:
+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
|
||||
Reference in New Issue
Block a user