feat: add provider login flow
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
package com.flagship.abox.manager.session.credential
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AndroidKeystoreCredentialCipherTest {
|
||||
@Test
|
||||
fun encryptsWithRandomIvAndProfileBoundAad() {
|
||||
val cipher = AndroidKeystoreCredentialCipher(
|
||||
keyAlias = "abox_manager_test_${System.nanoTime()}",
|
||||
)
|
||||
val profile = ProviderProfileId("profile")
|
||||
val plaintext = "secret".toByteArray()
|
||||
|
||||
val first = cipher.encrypt(profile, plaintext)
|
||||
val second = cipher.encrypt(profile, plaintext)
|
||||
|
||||
assertFalse(first.contentEquals(second))
|
||||
assertArrayEquals(plaintext, cipher.decrypt(profile, first))
|
||||
assertArrayEquals(plaintext, cipher.decrypt(profile, second))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<manifest />
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.flagship.abox.manager.session.credential
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
class AndroidKeystoreCredentialCipher(
|
||||
private val keyAlias: String = KEY_ALIAS,
|
||||
) : CredentialCipher {
|
||||
override fun encrypt(profileId: ProviderProfileId, plaintext: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey())
|
||||
cipher.updateAAD(profileId.value.toByteArray(Charsets.UTF_8))
|
||||
val ciphertext = cipher.doFinal(plaintext)
|
||||
return ByteBuffer.allocate(2 + cipher.iv.size + ciphertext.size)
|
||||
.put(VERSION)
|
||||
.put(cipher.iv.size.toByte())
|
||||
.put(cipher.iv)
|
||||
.put(ciphertext)
|
||||
.array()
|
||||
}
|
||||
|
||||
override fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray {
|
||||
val buffer = ByteBuffer.wrap(encrypted)
|
||||
require(buffer.get() == VERSION) { "Unsupported credential format" }
|
||||
val ivLength = buffer.get().toInt() and 0xFF
|
||||
require(ivLength in 12..16 && buffer.remaining() > ivLength) { "Invalid credential payload" }
|
||||
val iv = ByteArray(ivLength).also(buffer::get)
|
||||
val ciphertext = ByteArray(buffer.remaining()).also(buffer::get)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey(), GCMParameterSpec(GCM_TAG_BITS, iv))
|
||||
cipher.updateAAD(profileId.value.toByteArray(Charsets.UTF_8))
|
||||
return cipher.doFinal(ciphertext)
|
||||
}
|
||||
|
||||
private fun secretKey(): SecretKey {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) }
|
||||
(keyStore.getKey(keyAlias, null) as? SecretKey)?.let { return it }
|
||||
return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE).run {
|
||||
init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
keyAlias,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
|
||||
)
|
||||
.setKeySize(KEY_SIZE_BITS)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build(),
|
||||
)
|
||||
generateKey()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val KEY_ALIAS = "abox_manager_credentials_v1"
|
||||
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
||||
private const val KEY_SIZE_BITS = 256
|
||||
private const val GCM_TAG_BITS = 128
|
||||
private const val VERSION: Byte = 1
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.flagship.abox.manager.session.credential
|
||||
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
|
||||
interface CredentialCipher {
|
||||
fun encrypt(profileId: ProviderProfileId, plaintext: ByteArray): ByteArray
|
||||
|
||||
fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.flagship.abox.manager.session.credential
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.flagship.abox.manager.domain.CredentialStore
|
||||
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.Password
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.Username
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.KeyStoreException
|
||||
import java.security.UnrecoverableKeyException
|
||||
import java.util.Base64
|
||||
import javax.crypto.AEADBadTagException
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class KeystoreCredentialStore(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
private val cipher: CredentialCipher,
|
||||
) : CredentialStore {
|
||||
override suspend fun saveCredentials(
|
||||
profileId: ProviderProfileId,
|
||||
credentials: LoginCredentials,
|
||||
): DomainResult<Unit> = operation {
|
||||
val encrypted = cipher.encrypt(profileId, encode(credentials))
|
||||
dataStore.edit { it[credentialKey(profileId)] = Base64.getEncoder().encodeToString(encrypted) }
|
||||
}
|
||||
|
||||
override suspend fun loadCredentials(
|
||||
profileId: ProviderProfileId,
|
||||
): DomainResult<LoginCredentials?> {
|
||||
return try {
|
||||
val value = dataStore.data.first()[credentialKey(profileId)]
|
||||
?: return DomainResult.Success(null)
|
||||
val encrypted = Base64.getDecoder().decode(value)
|
||||
DomainResult.Success(decode(cipher.decrypt(profileId, encrypted)))
|
||||
} catch (error: Exception) {
|
||||
if (error.isPermanentCredentialFailure()) {
|
||||
dataStore.edit { it.remove(credentialKey(profileId)) }
|
||||
DomainResult.Success(null)
|
||||
} else {
|
||||
DomainResult.Failure(
|
||||
DomainError.Unknown(detail = error.message ?: error::class.java.simpleName),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearCredentials(profileId: ProviderProfileId): DomainResult<Unit> = operation {
|
||||
dataStore.edit { it.remove(credentialKey(profileId)) }
|
||||
}
|
||||
|
||||
private fun encode(credentials: LoginCredentials): ByteArray = ByteArrayOutputStream().use { output ->
|
||||
DataOutputStream(output).use { data ->
|
||||
data.writeUTF(credentials.username.value)
|
||||
data.writeUTF(credentials.password.value)
|
||||
}
|
||||
output.toByteArray()
|
||||
}
|
||||
|
||||
private fun decode(value: ByteArray): LoginCredentials = DataInputStream(
|
||||
ByteArrayInputStream(value),
|
||||
).use { data ->
|
||||
val username = data.readUTF()
|
||||
val password = data.readUTF()
|
||||
require(data.available() == 0) { "Unexpected credential data" }
|
||||
LoginCredentials(Username(username), Password(password))
|
||||
}
|
||||
|
||||
private suspend fun operation(block: suspend () -> Unit): DomainResult<Unit> = try {
|
||||
block()
|
||||
DomainResult.Success(Unit)
|
||||
} catch (error: Exception) {
|
||||
DomainResult.Failure(
|
||||
DomainError.Unknown(detail = error.message ?: error::class.java.simpleName),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun credentialKey(profileId: ProviderProfileId) =
|
||||
stringPreferencesKey("profile_${profileId.value}_credential")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.isPermanentCredentialFailure(): Boolean = when (this) {
|
||||
is AEADBadTagException,
|
||||
is InvalidKeyException,
|
||||
is KeyStoreException,
|
||||
is UnrecoverableKeyException,
|
||||
is IllegalArgumentException,
|
||||
-> true
|
||||
|
||||
else -> cause?.isPermanentCredentialFailure() == true
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.flagship.abox.manager.session.gateway
|
||||
|
||||
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 LocalSessionGatewayImpl : 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) }
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.flagship.abox.manager.session.gateway
|
||||
|
||||
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.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.Session
|
||||
import com.flagship.abox.manager.domain.SessionGateway
|
||||
import com.flagship.abox.manager.domain.SessionValidity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class UnavailableRemoteSessionGateway : SessionGateway {
|
||||
override fun observeSession(profileId: ProviderProfileId): Flow<Session?> = flowOf(null)
|
||||
|
||||
override suspend fun login(
|
||||
profileId: ProviderProfileId,
|
||||
credentials: LoginCredentials,
|
||||
): DomainResult<Session> = unavailable()
|
||||
|
||||
override suspend fun validateSession(
|
||||
profileId: ProviderProfileId,
|
||||
): DomainResult<SessionValidity> = unavailable()
|
||||
|
||||
override suspend fun logout(profileId: ProviderProfileId): DomainResult<Unit> =
|
||||
DomainResult.Success(Unit)
|
||||
|
||||
private fun <T> unavailable(): DomainResult<T> = DomainResult.Failure(
|
||||
DomainError.InvalidRequest(detail = "Remote providers are not enabled in this build"),
|
||||
)
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package com.flagship.abox.manager.session.profile
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
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 com.flagship.abox.manager.domain.ProviderType
|
||||
import com.flagship.abox.manager.domain.Username
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ProviderProfileStoreImpl(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : ProviderProfileStore {
|
||||
override fun observeProfiles(): Flow<List<ProviderProfile>> = dataStore.data.map(::profilesFrom)
|
||||
|
||||
override fun observeActiveProfile(): Flow<ProviderProfile?> = dataStore.data.map(::activeProfileFrom)
|
||||
|
||||
override fun observeLoginPreferences(profileId: ProviderProfileId): Flow<LoginPreferences> =
|
||||
dataStore.data.map { preferencesFrom(it, profileId) }
|
||||
|
||||
override suspend fun profiles(): List<ProviderProfile> = profilesFrom(dataStore.data.first())
|
||||
|
||||
override suspend fun activeProfile(): ProviderProfile? = activeProfileFrom(dataStore.data.first())
|
||||
|
||||
override suspend fun loginPreferences(profileId: ProviderProfileId): LoginPreferences =
|
||||
preferencesFrom(dataStore.data.first(), profileId)
|
||||
|
||||
override suspend fun saveProfile(profile: ProviderProfile): DomainResult<Unit> = operation {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[PROFILE_IDS] = preferences[PROFILE_IDS].orEmpty() + profile.id.value
|
||||
preferences[profileTypeKey(profile.id)] = profile.type.name
|
||||
preferences[profileNameKey(profile.id)] = profile.displayName
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setActiveProfile(profileId: ProviderProfileId?): DomainResult<Unit> = operation {
|
||||
dataStore.edit { preferences ->
|
||||
if (profileId == null) preferences.remove(ACTIVE_PROFILE_ID)
|
||||
else preferences[ACTIVE_PROFILE_ID] = profileId.value
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveLoginPreferences(
|
||||
preferences: LoginPreferences,
|
||||
): DomainResult<Unit> = operation {
|
||||
dataStore.edit { values ->
|
||||
val profileId = preferences.profileId
|
||||
values[rememberUsernameKey(profileId)] = preferences.rememberUsername
|
||||
values[autoLoginKey(profileId)] = preferences.autoLoginEnabled
|
||||
val username = preferences.rememberedUsername?.value
|
||||
if (username == null) values.remove(usernameKey(profileId))
|
||||
else values[usernameKey(profileId)] = username
|
||||
}
|
||||
}
|
||||
|
||||
private fun profilesFrom(preferences: Preferences): List<ProviderProfile> =
|
||||
preferences[PROFILE_IDS].orEmpty().mapNotNull { idValue ->
|
||||
val id = ProviderProfileId(idValue)
|
||||
val type = preferences[profileTypeKey(id)]
|
||||
?.let { runCatching { ProviderType.valueOf(it) }.getOrNull() }
|
||||
?: return@mapNotNull null
|
||||
val displayName = preferences[profileNameKey(id)] ?: return@mapNotNull null
|
||||
ProviderProfile(id, type, displayName)
|
||||
}.sortedBy { it.id.value }
|
||||
|
||||
private fun activeProfileFrom(preferences: Preferences): ProviderProfile? {
|
||||
val activeId = preferences[ACTIVE_PROFILE_ID] ?: return null
|
||||
return profilesFrom(preferences).firstOrNull { it.id.value == activeId }
|
||||
}
|
||||
|
||||
private fun preferencesFrom(
|
||||
preferences: Preferences,
|
||||
profileId: ProviderProfileId,
|
||||
): LoginPreferences = LoginPreferences(
|
||||
profileId = profileId,
|
||||
rememberedUsername = preferences[usernameKey(profileId)]?.let(::Username),
|
||||
rememberUsername = preferences[rememberUsernameKey(profileId)] ?: false,
|
||||
autoLoginEnabled = preferences[autoLoginKey(profileId)] ?: false,
|
||||
)
|
||||
|
||||
private suspend fun operation(block: suspend () -> Unit): DomainResult<Unit> = try {
|
||||
block()
|
||||
DomainResult.Success(Unit)
|
||||
} catch (error: Exception) {
|
||||
DomainResult.Failure(
|
||||
com.flagship.abox.manager.domain.DomainError.Unknown(
|
||||
detail = error.message ?: error::class.java.simpleName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val PROFILE_IDS = stringSetPreferencesKey("profile_ids")
|
||||
private val ACTIVE_PROFILE_ID = stringPreferencesKey("active_profile_id")
|
||||
|
||||
private fun profileTypeKey(id: ProviderProfileId) =
|
||||
stringPreferencesKey("profile_${id.value}_type")
|
||||
|
||||
private fun profileNameKey(id: ProviderProfileId) =
|
||||
stringPreferencesKey("profile_${id.value}_name")
|
||||
|
||||
private fun usernameKey(id: ProviderProfileId) =
|
||||
stringPreferencesKey("profile_${id.value}_username")
|
||||
|
||||
private fun rememberUsernameKey(id: ProviderProfileId) =
|
||||
booleanPreferencesKey("profile_${id.value}_remember_username")
|
||||
|
||||
private fun autoLoginKey(id: ProviderProfileId) =
|
||||
booleanPreferencesKey("profile_${id.value}_auto_login")
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.flagship.abox.manager.session.profile
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
|
||||
private const val SESSION_DATA_STORE_NAME = "provider_session"
|
||||
|
||||
val Context.sessionDataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = SESSION_DATA_STORE_NAME,
|
||||
)
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.flagship.abox.manager.session
|
||||
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
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 com.flagship.abox.manager.session.credential.CredentialCipher
|
||||
import com.flagship.abox.manager.session.credential.KeystoreCredentialStore
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class CredentialStoreTest {
|
||||
@Test
|
||||
fun storesClearsAndIsolatesCredentials() = runTest {
|
||||
val file = File.createTempFile("credentials", ".preferences_pb").apply { delete() }
|
||||
val dataStore = PreferenceDataStoreFactory.create(scope = backgroundScope) { file }
|
||||
val store = KeystoreCredentialStore(dataStore, ReversingCipher())
|
||||
val first = ProviderProfileId("alpha")
|
||||
val second = ProviderProfileId("bravo")
|
||||
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)
|
||||
}
|
||||
|
||||
private class ReversingCipher : CredentialCipher {
|
||||
override fun encrypt(profileId: ProviderProfileId, plaintext: ByteArray): ByteArray =
|
||||
(profileId.value.toByteArray() + byteArrayOf(0) + plaintext).reversedArray()
|
||||
|
||||
override fun decrypt(profileId: ProviderProfileId, encrypted: ByteArray): ByteArray {
|
||||
val decoded = encrypted.reversedArray()
|
||||
val delimiter = decoded.indexOf(0)
|
||||
require(decoded.copyOfRange(0, delimiter).decodeToString() == profileId.value)
|
||||
return decoded.copyOfRange(delimiter + 1, decoded.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.flagship.abox.manager.session
|
||||
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
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 com.flagship.abox.manager.session.profile.ProviderProfileStoreImpl
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ProviderProfileStoreTest {
|
||||
@Test
|
||||
fun persistsActiveProfileAndLoginPreferences() = runTest {
|
||||
val file = File.createTempFile("profiles", ".preferences_pb").apply { delete() }
|
||||
val dataStore = PreferenceDataStoreFactory.create(scope = backgroundScope) { file }
|
||||
val store = ProviderProfileStoreImpl(dataStore)
|
||||
val id = ProviderProfileId("abox")
|
||||
val profile = ProviderProfile(id, ProviderType.ABOX, "ABox")
|
||||
val preferences = LoginPreferences(id, Username("demo"), true, true)
|
||||
|
||||
assertEquals(DomainResult.Success(Unit), store.saveProfile(profile))
|
||||
store.setActiveProfile(id)
|
||||
store.saveLoginPreferences(preferences)
|
||||
|
||||
assertEquals(profile, store.observeActiveProfile().first())
|
||||
assertEquals(preferences, store.observeLoginPreferences(id).first())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user