mirror of
https://github.com/bitwarden/android.git
synced 2024-11-25 02:46:00 +03:00
Sync on database scheme change
Trigger a sync when the database schema changes. This ensures that the client and server are in sync after a schema update.
This commit is contained in:
parent
b5752c10ed
commit
c4fa2cb08a
9 changed files with 126 additions and 6 deletions
|
@ -74,6 +74,11 @@ interface SettingsDiskSource {
|
||||||
*/
|
*/
|
||||||
var lastDatabaseSchemeChangeInstant: Instant?
|
var lastDatabaseSchemeChangeInstant: Instant?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits updates that track [lastDatabaseSchemeChangeInstant].
|
||||||
|
*/
|
||||||
|
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears all the settings data for the given user.
|
* Clears all the settings data for the given user.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -71,6 +71,8 @@ class SettingsDiskSourceImpl(
|
||||||
|
|
||||||
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
private val mutableHasUserLoggedInOrCreatedAccountFlow = bufferedMutableSharedFlow<Boolean?>()
|
||||||
|
|
||||||
|
private val mutableLastDatabaseSchemeChangeInstantFlow = bufferedMutableSharedFlow<Instant?>()
|
||||||
|
|
||||||
private val mutableScreenCaptureAllowedFlowMap =
|
private val mutableScreenCaptureAllowedFlowMap =
|
||||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||||
|
|
||||||
|
@ -154,7 +156,14 @@ class SettingsDiskSourceImpl(
|
||||||
|
|
||||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
override var lastDatabaseSchemeChangeInstant: Instant?
|
||||||
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
|
get() = getLong(LAST_SCHEME_CHANGE_INSTANT)?.let { Instant.ofEpochMilli(it) }
|
||||||
set(value) = putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
|
set(value) {
|
||||||
|
putLong(LAST_SCHEME_CHANGE_INSTANT, value?.toEpochMilli())
|
||||||
|
mutableLastDatabaseSchemeChangeInstantFlow.tryEmit(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||||
|
get() = mutableLastDatabaseSchemeChangeInstantFlow
|
||||||
|
.onSubscription { emit(lastDatabaseSchemeChangeInstant) }
|
||||||
|
|
||||||
override fun clearData(userId: String) {
|
override fun clearData(userId: String) {
|
||||||
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
storeVaultTimeoutInMinutes(userId = userId, vaultTimeoutInMinutes = null)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.x8bit.bitwarden.data.platform.manager
|
package com.x8bit.bitwarden.data.platform.manager
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,4 +15,9 @@ interface DatabaseSchemeManager {
|
||||||
* that a scheme change to any database will update this value and trigger a sync.
|
* that a scheme change to any database will update this value and trigger a sync.
|
||||||
*/
|
*/
|
||||||
var lastDatabaseSchemeChangeInstant: Instant?
|
var lastDatabaseSchemeChangeInstant: Instant?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flow of the last database schema change instant.
|
||||||
|
*/
|
||||||
|
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package com.x8bit.bitwarden.data.platform.manager
|
package com.x8bit.bitwarden.data.platform.manager
|
||||||
|
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||||
|
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,10 +12,23 @@ import java.time.Instant
|
||||||
*/
|
*/
|
||||||
class DatabaseSchemeManagerImpl(
|
class DatabaseSchemeManagerImpl(
|
||||||
val settingsDiskSource: SettingsDiskSource,
|
val settingsDiskSource: SettingsDiskSource,
|
||||||
|
val dispatcherManager: DispatcherManager,
|
||||||
) : DatabaseSchemeManager {
|
) : DatabaseSchemeManager {
|
||||||
|
|
||||||
|
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
||||||
|
|
||||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
override var lastDatabaseSchemeChangeInstant: Instant?
|
||||||
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
|
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
|
||||||
set(value) {
|
set(value) {
|
||||||
settingsDiskSource.lastDatabaseSchemeChangeInstant = value
|
settingsDiskSource.lastDatabaseSchemeChangeInstant = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val lastDatabaseSchemeChangeInstantFlow =
|
||||||
|
settingsDiskSource
|
||||||
|
.lastDatabaseSchemeChangeInstantFlow
|
||||||
|
.stateIn(
|
||||||
|
scope = unconfinedScope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = settingsDiskSource.lastDatabaseSchemeChangeInstant,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -304,7 +304,9 @@ object PlatformManagerModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabaseSchemeManager(
|
fun provideDatabaseSchemeManager(
|
||||||
settingsDiskSource: SettingsDiskSource,
|
settingsDiskSource: SettingsDiskSource,
|
||||||
|
dispatcherManager: DispatcherManager,
|
||||||
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
|
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
|
||||||
settingsDiskSource = settingsDiskSource,
|
settingsDiskSource = settingsDiskSource,
|
||||||
|
dispatcherManager = dispatcherManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,6 +99,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
@ -320,6 +321,12 @@ class VaultRepositoryImpl(
|
||||||
.syncFolderUpsertFlow
|
.syncFolderUpsertFlow
|
||||||
.onEach(::syncFolderIfNecessary)
|
.onEach(::syncFolderIfNecessary)
|
||||||
.launchIn(ioScope)
|
.launchIn(ioScope)
|
||||||
|
|
||||||
|
databaseSchemeManager
|
||||||
|
.lastDatabaseSchemeChangeInstantFlow
|
||||||
|
.filterNotNull()
|
||||||
|
.onEach { sync() }
|
||||||
|
.launchIn(ioScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearUnlockedData() {
|
private fun clearUnlockedData() {
|
||||||
|
|
|
@ -42,6 +42,9 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||||
private val mutableScreenCaptureAllowedFlowMap =
|
private val mutableScreenCaptureAllowedFlowMap =
|
||||||
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
mutableMapOf<String, MutableSharedFlow<Boolean?>>()
|
||||||
|
|
||||||
|
private val mutableLastDatabaseSchemeChangeInstant =
|
||||||
|
bufferedMutableSharedFlow<Instant?>()
|
||||||
|
|
||||||
private var storedAppTheme: AppTheme = AppTheme.DEFAULT
|
private var storedAppTheme: AppTheme = AppTheme.DEFAULT
|
||||||
private val storedLastSyncTime = mutableMapOf<String, Instant?>()
|
private val storedLastSyncTime = mutableMapOf<String, Instant?>()
|
||||||
private val storedVaultTimeoutActions = mutableMapOf<String, VaultTimeoutAction?>()
|
private val storedVaultTimeoutActions = mutableMapOf<String, VaultTimeoutAction?>()
|
||||||
|
@ -137,6 +140,11 @@ class FakeSettingsDiskSource : SettingsDiskSource {
|
||||||
get() = storedLastDatabaseSchemeChangeInstant
|
get() = storedLastDatabaseSchemeChangeInstant
|
||||||
set(value) { storedLastDatabaseSchemeChangeInstant = value }
|
set(value) { storedLastDatabaseSchemeChangeInstant = value }
|
||||||
|
|
||||||
|
override val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
||||||
|
get() = mutableLastDatabaseSchemeChangeInstant.onSubscription {
|
||||||
|
emit(lastDatabaseSchemeChangeInstant)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAccountBiometricIntegrityValidity(
|
override fun getAccountBiometricIntegrityValidity(
|
||||||
userId: String,
|
userId: String,
|
||||||
systemBioIntegrityState: String,
|
systemBioIntegrityState: String,
|
||||||
|
|
|
@ -1,24 +1,37 @@
|
||||||
package com.x8bit.bitwarden.data.platform.manager
|
package com.x8bit.bitwarden.data.platform.manager
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.runs
|
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
class DatabaseSchemeManagerTest {
|
class DatabaseSchemeManagerTest {
|
||||||
|
|
||||||
|
private val mutableLastDatabaseSchemeChangeInstantFlow = MutableStateFlow<Instant?>(null)
|
||||||
private val mockSettingsDiskSource: SettingsDiskSource = mockk {
|
private val mockSettingsDiskSource: SettingsDiskSource = mockk {
|
||||||
every { lastDatabaseSchemeChangeInstant } returns null
|
every {
|
||||||
every { lastDatabaseSchemeChangeInstant = any() } just runs
|
lastDatabaseSchemeChangeInstant
|
||||||
|
} returns mutableLastDatabaseSchemeChangeInstantFlow.value
|
||||||
|
every { lastDatabaseSchemeChangeInstant = any() } answers {
|
||||||
|
mutableLastDatabaseSchemeChangeInstantFlow.value = firstArg()
|
||||||
}
|
}
|
||||||
|
every {
|
||||||
|
lastDatabaseSchemeChangeInstantFlow
|
||||||
|
} returns mutableLastDatabaseSchemeChangeInstantFlow
|
||||||
|
}
|
||||||
|
private val dispatcherManager = FakeDispatcherManager()
|
||||||
private val databaseSchemeManager = DatabaseSchemeManagerImpl(
|
private val databaseSchemeManager = DatabaseSchemeManagerImpl(
|
||||||
settingsDiskSource = mockSettingsDiskSource,
|
settingsDiskSource = mockSettingsDiskSource,
|
||||||
|
dispatcherManager = dispatcherManager,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
|
@ -30,6 +43,23 @@ class DatabaseSchemeManagerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `setLastDatabaseSchemeChangeInstant does emit value`() = runTest {
|
||||||
|
databaseSchemeManager.lastDatabaseSchemeChangeInstantFlow.test {
|
||||||
|
// Assert the value is initialized to null
|
||||||
|
assertEquals(
|
||||||
|
null,
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
// Assert the new value is emitted
|
||||||
|
databaseSchemeManager.lastDatabaseSchemeChangeInstant = FIXED_CLOCK.instant()
|
||||||
|
assertEquals(
|
||||||
|
FIXED_CLOCK.instant(),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `getLastDatabaseSchemeChangeInstant retrieves stored value from settingsDiskSource`() {
|
fun `getLastDatabaseSchemeChangeInstant retrieves stored value from settingsDiskSource`() {
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
||||||
|
|
|
@ -190,8 +190,14 @@ class VaultRepositoryTest {
|
||||||
mutableUnlockedUserIdsStateFlow.first { userId in it }
|
mutableUnlockedUserIdsStateFlow.first { userId in it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val mutableLastDatabaseSchemeChangeInstantFlow = MutableStateFlow<Instant?>(null)
|
||||||
private val databaseSchemeManager: DatabaseSchemeManager = mockk {
|
private val databaseSchemeManager: DatabaseSchemeManager = mockk {
|
||||||
every { lastDatabaseSchemeChangeInstant } returns null
|
every {
|
||||||
|
lastDatabaseSchemeChangeInstant
|
||||||
|
} returns mutableLastDatabaseSchemeChangeInstantFlow.value
|
||||||
|
every {
|
||||||
|
lastDatabaseSchemeChangeInstantFlow
|
||||||
|
} returns mutableLastDatabaseSchemeChangeInstantFlow
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
|
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
|
||||||
|
@ -777,6 +783,36 @@ class VaultRepositoryTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `lastDatabaseSchemeChangeInstantFlow should trigger sync when new value is not null`() =
|
||||||
|
runTest {
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
every {
|
||||||
|
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
||||||
|
} returns mutableLastDatabaseSchemeChangeInstantFlow.value
|
||||||
|
coEvery { syncService.sync() } just awaits
|
||||||
|
|
||||||
|
mutableLastDatabaseSchemeChangeInstantFlow.value = clock.instant()
|
||||||
|
|
||||||
|
coVerify(exactly = 1) { syncService.sync() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `lastDatabaseSchemeChangeInstantFlow should not sync when new value is null`() =
|
||||||
|
runTest {
|
||||||
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
|
|
||||||
|
every {
|
||||||
|
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
||||||
|
} returns mutableLastDatabaseSchemeChangeInstantFlow.value
|
||||||
|
|
||||||
|
coEvery { syncService.sync() } just awaits
|
||||||
|
|
||||||
|
mutableLastDatabaseSchemeChangeInstantFlow.value = null
|
||||||
|
|
||||||
|
coVerify(exactly = 0) { syncService.sync() }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and VaultDiskSource`() =
|
fun `sync with syncService Success should unlock the vault for orgs if necessary and update AuthDiskSource and VaultDiskSource`() =
|
||||||
|
|
Loading…
Reference in a new issue