Add disk storage for Vault Timeout (#518)

This commit is contained in:
Brian Yencho 2024-01-07 11:21:34 -06:00 committed by Álison Fernandes
parent f54af724b1
commit 54c288cb25
5 changed files with 244 additions and 0 deletions

View file

@ -10,6 +10,37 @@ import androidx.core.content.edit
abstract class BaseDiskSource(
private val sharedPreferences: SharedPreferences,
) {
/**
* Gets the [Int] for the given [key] from [SharedPreferences], or return the [default] value
* if that key is not present.
*/
protected fun getInt(
key: String,
default: Int? = null,
): Int? =
if (sharedPreferences.contains(key)) {
sharedPreferences.getInt(key, 0)
} else {
// Make sure we can return a null value as a default if necessary
default
}
/**
* Puts the [value] in [SharedPreferences] for the given [key] (or removes the key when the
* value is `null`).
*/
protected fun putInt(
key: String,
value: Int?,
): Unit =
sharedPreferences.edit {
if (value != null) {
putInt(key, value)
} else {
remove(key)
}
}
protected fun getString(
key: String,
default: String? = null,

View file

@ -0,0 +1,25 @@
package com.x8bit.bitwarden.data.platform.datasource.disk
import kotlinx.coroutines.flow.Flow
/**
* Primary access point for general settings-related disk information.
*/
interface SettingsDiskSource {
/**
* Gets the current vault timeout (in minutes) for the given [userId] (or `null` if the vault
* should never time out).
*/
fun getVaultTimeoutInMinutes(userId: String): Int?
/**
* Emits updates that track [getVaultTimeoutInMinutes] for the given [userId]. This will replay
* the last known value, if any.
*/
fun getVaultTimeoutInMinutesFlow(userId: String): Flow<Int?>
/**
* Stores the given [vaultTimeoutInMinutes] for the given [userId].
*/
fun storeVaultTimeoutInMinutes(userId: String, vaultTimeoutInMinutes: Int?)
}

View file

@ -0,0 +1,46 @@
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.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
private const val VAULT_TIME_IN_MINUTES_KEY = "$BASE_KEY:vaultTimeout"
/**
* Primary implementation of [SettingsDiskSource].
*/
class SettingsDiskSourceImpl(
val sharedPreferences: SharedPreferences,
) : BaseDiskSource(sharedPreferences = sharedPreferences),
SettingsDiskSource {
private val mutableVaultTimeoutInMinutesFlowMap =
mutableMapOf<String, MutableSharedFlow<Int?>>()
override fun getVaultTimeoutInMinutes(userId: String): Int? =
getInt(key = "${VAULT_TIME_IN_MINUTES_KEY}_$userId")
override fun getVaultTimeoutInMinutesFlow(userId: String): Flow<Int?> =
getMutableVaultTimeoutInMinutesFlow(userId = userId)
.onSubscription { emit(getVaultTimeoutInMinutes(userId = userId)) }
override fun storeVaultTimeoutInMinutes(
userId: String,
vaultTimeoutInMinutes: Int?,
) {
putInt(
key = "${VAULT_TIME_IN_MINUTES_KEY}_$userId",
value = vaultTimeoutInMinutes,
)
getMutableVaultTimeoutInMinutesFlow(userId = userId).tryEmit(vaultTimeoutInMinutes)
}
private fun getMutableVaultTimeoutInMinutesFlow(
userId: String,
): MutableSharedFlow<Int?> =
mutableVaultTimeoutInMinutesFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
}

View file

@ -0,0 +1,98 @@
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 kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class SettingsDiskSourceTest {
private val fakeSharedPreferences = FakeSharedPreferences()
private val settingsDiskSource = SettingsDiskSourceImpl(
sharedPreferences = fakeSharedPreferences,
)
@Test
fun `getVaultTimeoutInMinutes when values are present should pull from SharedPreferences`() {
val vaultTimeoutBaseKey = "bwPreferencesStorage:vaultTimeout"
val mockUserId = "mockUserId"
val vaultTimeoutInMinutes = 360
fakeSharedPreferences
.edit()
.putInt(
"${vaultTimeoutBaseKey}_$mockUserId",
vaultTimeoutInMinutes,
)
.apply()
val actual = settingsDiskSource.getVaultTimeoutInMinutes(userId = mockUserId)
assertEquals(
vaultTimeoutInMinutes,
actual,
)
}
@Test
fun `getVaultTimeoutInMinutes when values are absent should return null`() {
val mockUserId = "mockUserId"
assertNull(settingsDiskSource.getVaultTimeoutInMinutes(userId = mockUserId))
}
@Test
fun `getVaultTimeoutInMinutesFlow should react to changes in getOrganizations`() = runTest {
val mockUserId = "mockUserId"
val vaultTimeoutInMinutes = 360
settingsDiskSource.getVaultTimeoutInMinutesFlow(userId = mockUserId).test {
// The initial values of the Flow and the property are in sync
assertNull(settingsDiskSource.getVaultTimeoutInMinutes(userId = mockUserId))
assertNull(awaitItem())
// Updating the repository updates shared preferences
settingsDiskSource.storeVaultTimeoutInMinutes(
userId = mockUserId,
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
)
assertEquals(vaultTimeoutInMinutes, awaitItem())
}
}
@Test
fun `storeVaultTimeoutInMinutes for non-null values should update SharedPreferences`() {
val vaultTimeoutBaseKey = "bwPreferencesStorage:vaultTimeout"
val mockUserId = "mockUserId"
val vaultTimeoutInMinutes = 360
settingsDiskSource.storeVaultTimeoutInMinutes(
userId = mockUserId,
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
)
val actual = fakeSharedPreferences.getInt(
"${vaultTimeoutBaseKey}_$mockUserId",
0,
)
assertEquals(
vaultTimeoutInMinutes,
actual,
)
}
@Test
fun `storeVaultTimeoutInMinutes for null values should clear SharedPreferences`() {
val vaultTimeoutBaseKey = "bwPreferencesStorage:vaultTimeout"
val mockUserId = "mockUserId"
val previousValue = 123
val vaultTimeoutKey = "${vaultTimeoutBaseKey}_$mockUserId"
fakeSharedPreferences.edit {
putInt(vaultTimeoutKey, previousValue)
}
assertTrue(fakeSharedPreferences.contains(vaultTimeoutKey))
settingsDiskSource.storeVaultTimeoutInMinutes(
userId = mockUserId,
vaultTimeoutInMinutes = null,
)
assertFalse(fakeSharedPreferences.contains(vaultTimeoutKey))
}
}

View file

@ -0,0 +1,44 @@
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.util.bufferedMutableSharedFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.onSubscription
/**
* Fake, memory-based implementation of [SettingsDiskSource].
*/
class FakeSettingsDiskSource : SettingsDiskSource {
private val mutableVaultTimeoutInMinutesFlowMap =
mutableMapOf<String, MutableSharedFlow<Int?>>()
private val storedVaultTimeoutInMinutes = mutableMapOf<String, Int?>()
override fun getVaultTimeoutInMinutes(userId: String): Int? =
storedVaultTimeoutInMinutes[userId]
override fun getVaultTimeoutInMinutesFlow(userId: String): Flow<Int?> =
getMutableVaultTimeoutInMinutesFlow(userId = userId)
.onSubscription { emit(getVaultTimeoutInMinutes(userId = userId)) }
override fun storeVaultTimeoutInMinutes(
userId: String,
vaultTimeoutInMinutes: Int?,
) {
storedVaultTimeoutInMinutes[userId] = vaultTimeoutInMinutes
getMutableVaultTimeoutInMinutesFlow(userId = userId).tryEmit(vaultTimeoutInMinutes)
}
//region Private helper functions
private fun getMutableVaultTimeoutInMinutesFlow(
userId: String,
): MutableSharedFlow<Int?> =
mutableVaultTimeoutInMinutesFlowMap.getOrPut(userId) {
bufferedMutableSharedFlow(replay = 1)
}
//endregion Private helper functions
}