feat: add provider login flow
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
package com.flagship.abox.manager.testing.fakes
|
||||
|
||||
import com.flagship.abox.manager.domain.CredentialStore
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FakeCredentialStore : CredentialStore {
|
||||
private val values = ConcurrentHashMap<ProviderProfileId, LoginCredentials>()
|
||||
|
||||
override suspend fun saveCredentials(
|
||||
profileId: ProviderProfileId,
|
||||
credentials: LoginCredentials,
|
||||
): DomainResult<Unit> {
|
||||
values[profileId] = credentials
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun loadCredentials(
|
||||
profileId: ProviderProfileId,
|
||||
): DomainResult<LoginCredentials?> = DomainResult.Success(values[profileId])
|
||||
|
||||
override suspend fun clearCredentials(profileId: ProviderProfileId): DomainResult<Unit> {
|
||||
values.remove(profileId)
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.flagship.abox.manager.testing.fakes
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.LocalSessionGateway
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.Session
|
||||
import com.flagship.abox.manager.domain.SessionValidity
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeLocalSessionGateway : LocalSessionGateway {
|
||||
private val sessions = ConcurrentHashMap<ProviderProfileId, MutableStateFlow<Session?>>()
|
||||
|
||||
override fun observeSession(profileId: ProviderProfileId): Flow<Session?> = flow(profileId)
|
||||
|
||||
override suspend fun login(
|
||||
profileId: ProviderProfileId,
|
||||
credentials: LoginCredentials,
|
||||
): DomainResult<Session> = DomainResult.Success(
|
||||
Session(profileId, credentials.username).also { flow(profileId).value = it },
|
||||
)
|
||||
|
||||
override suspend fun validateSession(
|
||||
profileId: ProviderProfileId,
|
||||
): DomainResult<SessionValidity> = DomainResult.Success(
|
||||
if (flow(profileId).value == null) SessionValidity.INVALID else SessionValidity.VALID,
|
||||
)
|
||||
|
||||
override suspend fun logout(profileId: ProviderProfileId): DomainResult<Unit> {
|
||||
flow(profileId).value = null
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
private fun flow(profileId: ProviderProfileId): MutableStateFlow<Session?> =
|
||||
sessions.getOrPut(profileId) { MutableStateFlow(null) }
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.flagship.abox.manager.testing.fakes
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.LoginPreferences
|
||||
import com.flagship.abox.manager.domain.ProviderProfile
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.ProviderProfileStore
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeProviderProfileStore : ProviderProfileStore {
|
||||
private val profileValues = ConcurrentHashMap<ProviderProfileId, ProviderProfile>()
|
||||
private val preferenceValues = ConcurrentHashMap<ProviderProfileId, LoginPreferences>()
|
||||
private val profilesFlow = MutableStateFlow<List<ProviderProfile>>(emptyList())
|
||||
private val activeProfileFlow = MutableStateFlow<ProviderProfile?>(null)
|
||||
private val preferenceFlows = ConcurrentHashMap<ProviderProfileId, MutableStateFlow<LoginPreferences>>()
|
||||
|
||||
override fun observeProfiles(): Flow<List<ProviderProfile>> = profilesFlow
|
||||
|
||||
override fun observeActiveProfile(): Flow<ProviderProfile?> = activeProfileFlow
|
||||
|
||||
override fun observeLoginPreferences(profileId: ProviderProfileId): Flow<LoginPreferences> =
|
||||
preferenceFlow(profileId)
|
||||
|
||||
override suspend fun profiles(): List<ProviderProfile> = profilesFlow.value
|
||||
|
||||
override suspend fun activeProfile(): ProviderProfile? = activeProfileFlow.value
|
||||
|
||||
override suspend fun loginPreferences(profileId: ProviderProfileId): LoginPreferences =
|
||||
preferenceFlow(profileId).value
|
||||
|
||||
override suspend fun saveProfile(profile: ProviderProfile): DomainResult<Unit> {
|
||||
profileValues[profile.id] = profile
|
||||
profilesFlow.value = profileValues.values.sortedBy { it.id.value }
|
||||
if (activeProfileFlow.value?.id == profile.id) activeProfileFlow.value = profile
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun setActiveProfile(profileId: ProviderProfileId?): DomainResult<Unit> {
|
||||
activeProfileFlow.value = profileId?.let(profileValues::get)
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun saveLoginPreferences(preferences: LoginPreferences): DomainResult<Unit> {
|
||||
preferenceValues[preferences.profileId] = preferences
|
||||
preferenceFlow(preferences.profileId).value = preferences
|
||||
return DomainResult.Success(Unit)
|
||||
}
|
||||
|
||||
private fun preferenceFlow(profileId: ProviderProfileId): MutableStateFlow<LoginPreferences> =
|
||||
preferenceFlows.getOrPut(profileId) {
|
||||
MutableStateFlow(
|
||||
preferenceValues[profileId] ?: LoginPreferences(profileId),
|
||||
)
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.flagship.abox.manager.testing.fakes
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.Password
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.Username
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class FakeCredentialStoreTest {
|
||||
@Test
|
||||
fun credentialsAreIsolatedByProfileAndClearIndependently() = runTest {
|
||||
val store = FakeCredentialStore()
|
||||
val first = ProviderProfileId("first")
|
||||
val second = ProviderProfileId("second")
|
||||
val firstCredentials = LoginCredentials(Username("one"), Password("secret-one"))
|
||||
val secondCredentials = LoginCredentials(Username("two"), Password("secret-two"))
|
||||
|
||||
store.saveCredentials(first, firstCredentials)
|
||||
store.saveCredentials(second, secondCredentials)
|
||||
store.clearCredentials(first)
|
||||
|
||||
assertNull((store.loadCredentials(first) as DomainResult.Success).value)
|
||||
assertEquals(secondCredentials, (store.loadCredentials(second) as DomainResult.Success).value)
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.flagship.abox.manager.testing.fakes
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.LoginPreferences
|
||||
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.Username
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class FakeProviderProfileStoreTest {
|
||||
@Test
|
||||
fun storesProfilesActiveSelectionAndPreferences() = runTest {
|
||||
val store = FakeProviderProfileStore()
|
||||
val id = ProviderProfileId("local")
|
||||
val profile = ProviderProfile(id, ProviderType.LOCAL_EXPERIENCE, "Local")
|
||||
val preferences = LoginPreferences(id, Username("demo"), true, false)
|
||||
|
||||
assertEquals(DomainResult.Success(Unit), store.saveProfile(profile))
|
||||
store.setActiveProfile(id)
|
||||
store.saveLoginPreferences(preferences)
|
||||
|
||||
assertEquals(listOf(profile), store.observeProfiles().first())
|
||||
assertEquals(profile, store.observeActiveProfile().first())
|
||||
assertEquals(preferences, store.observeLoginPreferences(id).first())
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package com.flagship.abox.manager.testing.fakes
|
||||
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.LoginPreferences
|
||||
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.Username
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class SessionCoordinatorTest {
|
||||
private val remoteProfile = ProviderProfile(
|
||||
ProviderProfileId("abox"),
|
||||
ProviderType.ABOX,
|
||||
"ABox",
|
||||
)
|
||||
private val localProfile = ProviderProfile(
|
||||
ProviderProfileId("local"),
|
||||
ProviderType.LOCAL_EXPERIENCE,
|
||||
"Local",
|
||||
)
|
||||
private val credentials = LoginCredentials(Username("demo"), Password("secret"))
|
||||
|
||||
@Test
|
||||
fun localProviderCreatesSessionWithoutRemoteLogin() = runTest {
|
||||
val fixture = Fixture()
|
||||
|
||||
val result = fixture.coordinator.selectProvider(localProfile)
|
||||
|
||||
assertTrue(result is DomainResult.Success)
|
||||
assertEquals(localProfile.id, fixture.coordinator.observeActiveSession().first()?.profileId)
|
||||
assertNull(fixture.remote.observeSession(localProfile.id).first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loginPersistsIndependentUsernameAndAutoLoginChoices() = runTest {
|
||||
val fixture = Fixture()
|
||||
fixture.coordinator.selectProvider(remoteProfile)
|
||||
|
||||
fixture.coordinator.login(
|
||||
remoteProfile.id,
|
||||
credentials,
|
||||
rememberUsername = false,
|
||||
autoLogin = true,
|
||||
)
|
||||
|
||||
val preferences = fixture.profiles.loginPreferences(remoteProfile.id)
|
||||
assertNull(preferences.rememberedUsername)
|
||||
assertTrue(preferences.autoLoginEnabled)
|
||||
assertEquals(
|
||||
credentials,
|
||||
(fixture.credentials.loadCredentials(remoteProfile.id) as DomainResult.Success).value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bootstrapUsesSavedAutoLoginCredentials() = runTest {
|
||||
val fixture = Fixture()
|
||||
fixture.profiles.saveProfile(remoteProfile)
|
||||
fixture.profiles.setActiveProfile(remoteProfile.id)
|
||||
fixture.profiles.saveLoginPreferences(
|
||||
LoginPreferences(remoteProfile.id, autoLoginEnabled = true),
|
||||
)
|
||||
fixture.credentials.saveCredentials(remoteProfile.id, credentials)
|
||||
|
||||
val result = fixture.coordinator.bootstrap()
|
||||
|
||||
assertEquals(remoteProfile.id, (result as DomainResult.Success).value?.profileId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remoteLogoutClearsPasswordButKeepsProfileAndUsername() = runTest {
|
||||
val fixture = Fixture()
|
||||
fixture.coordinator.selectProvider(remoteProfile)
|
||||
fixture.coordinator.login(
|
||||
remoteProfile.id,
|
||||
credentials,
|
||||
rememberUsername = true,
|
||||
autoLogin = true,
|
||||
)
|
||||
|
||||
fixture.coordinator.logout()
|
||||
|
||||
assertEquals(remoteProfile, fixture.profiles.activeProfile())
|
||||
val preferences = fixture.profiles.loginPreferences(remoteProfile.id)
|
||||
assertEquals(credentials.username, preferences.rememberedUsername)
|
||||
assertTrue(!preferences.autoLoginEnabled)
|
||||
assertNull(
|
||||
(fixture.credentials.loadCredentials(remoteProfile.id) as DomainResult.Success).value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun authenticationFailureDuringRecoveryClearsSavedCredential() = runTest {
|
||||
val fixture = Fixture()
|
||||
fixture.profiles.saveProfile(remoteProfile)
|
||||
fixture.profiles.setActiveProfile(remoteProfile.id)
|
||||
fixture.profiles.saveLoginPreferences(
|
||||
LoginPreferences(remoteProfile.id, autoLoginEnabled = true),
|
||||
)
|
||||
fixture.credentials.saveCredentials(remoteProfile.id, credentials)
|
||||
fixture.remote.failLogin(remoteProfile.id, DomainError.Authentication())
|
||||
|
||||
val result = fixture.coordinator.recoverSession(remoteProfile.id)
|
||||
|
||||
assertTrue(result is DomainResult.Failure)
|
||||
assertNull(
|
||||
(fixture.credentials.loadCredentials(remoteProfile.id) as DomainResult.Success).value,
|
||||
)
|
||||
assertTrue(!fixture.profiles.loginPreferences(remoteProfile.id).autoLoginEnabled)
|
||||
}
|
||||
|
||||
private class Fixture {
|
||||
val profiles = FakeProviderProfileStore()
|
||||
val credentials = FakeCredentialStore()
|
||||
val remote = FakeSessionGateway()
|
||||
private val local = FakeLocalSessionGateway()
|
||||
val coordinator = SessionCoordinator(profiles, credentials, remote, local)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user