Add VaultTimeoutAction and handle its persistence (#520)

This commit is contained in:
Brian Yencho 2024-01-07 13:06:30 -06:00 committed by Álison Fernandes
parent 6acfb10709
commit e2c35fc373
5 changed files with 179 additions and 0 deletions

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import kotlinx.coroutines.flow.Flow
/**
@ -22,4 +23,23 @@ interface SettingsDiskSource {
* Stores the given [vaultTimeoutInMinutes] for the given [userId].
*/
fun storeVaultTimeoutInMinutes(userId: String, vaultTimeoutInMinutes: Int?)
/**
* Gets the current [VaultTimeoutAction] for the given [userId].
*/
fun getVaultTimeoutAction(userId: String): VaultTimeoutAction?
/**
* Emits updates that track [getVaultTimeoutAction] for the given [userId]. This will replay
* the last known value, if any.
*/
fun getVaultTimeoutActionFlow(userId: String): Flow<VaultTimeoutAction?>
/**
* Stores the given [vaultTimeoutAction] for the given [userId].
*/
fun storeVaultTimeoutAction(
userId: String,
vaultTimeoutAction: VaultTimeoutAction?,
)
}

View file

@ -2,11 +2,13 @@ package com.x8bit.bitwarden.data.platform.datasource.disk
import android.content.SharedPreferences
import com.x8bit.bitwarden.data.platform.datasource.disk.BaseDiskSource.Companion.BASE_KEY
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
private const val VAULT_TIMEOUT_ACTION_KEY = "$BASE_KEY:vaultTimeoutAction"
private const val VAULT_TIME_IN_MINUTES_KEY = "$BASE_KEY:vaultTimeout"
/**
@ -16,6 +18,9 @@ class SettingsDiskSourceImpl(
val sharedPreferences: SharedPreferences,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
SettingsDiskSource {
private val mutableVaultTimeoutActionFlowMap =
mutableMapOf<String, MutableSharedFlow<VaultTimeoutAction?>>()
private val mutableVaultTimeoutInMinutesFlowMap =
mutableMapOf<String, MutableSharedFlow<Int?>>()
@ -37,6 +42,33 @@ class SettingsDiskSourceImpl(
getMutableVaultTimeoutInMinutesFlow(userId = userId).tryEmit(vaultTimeoutInMinutes)
}
override fun getVaultTimeoutAction(userId: String): VaultTimeoutAction? =
getInt(key = "${VAULT_TIMEOUT_ACTION_KEY}_$userId")?.let { storedValue ->
VaultTimeoutAction.entries.firstOrNull { storedValue == it.value }
}
override fun getVaultTimeoutActionFlow(userId: String): Flow<VaultTimeoutAction?> =
getMutableVaultTimeoutActionFlow(userId = userId)
.onSubscription { emit(getVaultTimeoutAction(userId = userId)) }
override fun storeVaultTimeoutAction(
userId: String,
vaultTimeoutAction: VaultTimeoutAction?,
) {
putInt(
key = "${VAULT_TIMEOUT_ACTION_KEY}_$userId",
value = vaultTimeoutAction?.value,
)
getMutableVaultTimeoutActionFlow(userId = userId).tryEmit(vaultTimeoutAction)
}
private fun getMutableVaultTimeoutActionFlow(
userId: String,
): MutableSharedFlow<VaultTimeoutAction?> =
mutableVaultTimeoutActionFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultTimeoutInMinutesFlow(
userId: String,
): MutableSharedFlow<Int?> =

View file

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.data.platform.repository.model
/**
* Represents different type of actions that may be performed when a vault times out.
*
* The [value] is used for consistent storage purposes.
*/
enum class VaultTimeoutAction(
val value: Int,
) {
/**
* The vault should lock when it times out.
*/
LOCK(0),
/**
* The user should be logged out when their vault times out.
*/
LOGOUT(1),
}

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.data.platform.datasource.disk
import androidx.core.content.edit
import app.cash.turbine.test
import com.x8bit.bitwarden.data.platform.base.FakeSharedPreferences
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@ -95,4 +96,83 @@ class SettingsDiskSourceTest {
)
assertFalse(fakeSharedPreferences.contains(vaultTimeoutKey))
}
@Test
fun `getVaultTimeoutAction when values are present should pull from SharedPreferences`() {
val vaultTimeoutActionBaseKey = "bwPreferencesStorage:vaultTimeoutAction"
val mockUserId = "mockUserId"
val vaultTimeoutAction = VaultTimeoutAction.LOCK
fakeSharedPreferences
.edit()
.putInt(
"${vaultTimeoutActionBaseKey}_$mockUserId",
vaultTimeoutAction.value,
)
.apply()
val actual = settingsDiskSource.getVaultTimeoutAction(userId = mockUserId)
assertEquals(
vaultTimeoutAction,
actual,
)
}
@Test
fun `getVaultTimeoutAction when values are absent should return null`() {
val mockUserId = "mockUserId"
assertNull(settingsDiskSource.getVaultTimeoutAction(userId = mockUserId))
}
@Test
fun `getVaultTimeoutActionFlow should react to changes in getOrganizations`() = runTest {
val mockUserId = "mockUserId"
val vaultTimeoutAction = VaultTimeoutAction.LOCK
settingsDiskSource.getVaultTimeoutActionFlow(userId = mockUserId).test {
// The initial values of the Flow and the property are in sync
assertNull(settingsDiskSource.getVaultTimeoutAction(userId = mockUserId))
assertNull(awaitItem())
// Updating the disk source updates shared preferences
settingsDiskSource.storeVaultTimeoutAction(
userId = mockUserId,
vaultTimeoutAction = vaultTimeoutAction,
)
assertEquals(vaultTimeoutAction, awaitItem())
}
}
@Test
fun `storeVaultTimeoutAction for non-null values should update SharedPreferences`() {
val vaultTimeoutActionBaseKey = "bwPreferencesStorage:vaultTimeoutAction"
val mockUserId = "mockUserId"
val vaultTimeoutAction = VaultTimeoutAction.LOCK
settingsDiskSource.storeVaultTimeoutAction(
userId = mockUserId,
vaultTimeoutAction = vaultTimeoutAction,
)
val actual = fakeSharedPreferences.getInt(
"${vaultTimeoutActionBaseKey}_$mockUserId",
0,
)
assertEquals(
vaultTimeoutAction.value,
actual,
)
}
@Test
fun `storeVaultTimeoutAction for null values should clear SharedPreferences`() {
val vaultTimeoutActionBaseKey = "bwPreferencesStorage:vaultTimeoutAction"
val mockUserId = "mockUserId"
val previousValue = VaultTimeoutAction.LOCK
val vaultTimeoutActionKey = "${vaultTimeoutActionBaseKey}_$mockUserId"
fakeSharedPreferences.edit {
putInt(vaultTimeoutActionKey, previousValue.value)
}
assertTrue(fakeSharedPreferences.contains(vaultTimeoutActionKey))
settingsDiskSource.storeVaultTimeoutAction(
userId = mockUserId,
vaultTimeoutAction = null,
)
assertFalse(fakeSharedPreferences.contains(vaultTimeoutActionKey))
}
}

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.disk.util
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -11,9 +12,13 @@ import kotlinx.coroutines.flow.onSubscription
*/
class FakeSettingsDiskSource : SettingsDiskSource {
private val mutableVaultTimeoutActionsFlowMap =
mutableMapOf<String, MutableSharedFlow<VaultTimeoutAction?>>()
private val mutableVaultTimeoutInMinutesFlowMap =
mutableMapOf<String, MutableSharedFlow<Int?>>()
private val storedVaultTimeoutActions = mutableMapOf<String, VaultTimeoutAction?>()
private val storedVaultTimeoutInMinutes = mutableMapOf<String, Int?>()
override fun getVaultTimeoutInMinutes(userId: String): Int? =
@ -31,8 +36,30 @@ class FakeSettingsDiskSource : SettingsDiskSource {
getMutableVaultTimeoutInMinutesFlow(userId = userId).tryEmit(vaultTimeoutInMinutes)
}
override fun getVaultTimeoutAction(userId: String): VaultTimeoutAction? =
storedVaultTimeoutActions[userId]
override fun getVaultTimeoutActionFlow(userId: String): Flow<VaultTimeoutAction?> =
getMutableVaultTimeoutActionsFlow(userId = userId)
.onSubscription { emit(getVaultTimeoutAction(userId = userId)) }
override fun storeVaultTimeoutAction(
userId: String,
vaultTimeoutAction: VaultTimeoutAction?,
) {
storedVaultTimeoutActions[userId] = vaultTimeoutAction
getMutableVaultTimeoutActionsFlow(userId = userId).tryEmit(vaultTimeoutAction)
}
//region Private helper functions
private fun getMutableVaultTimeoutActionsFlow(
userId: String,
): MutableSharedFlow<VaultTimeoutAction?> =
mutableVaultTimeoutActionsFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
private fun getMutableVaultTimeoutInMinutesFlow(
userId: String,
): MutableSharedFlow<Int?> =