mirror of
https://github.com/bitwarden/android.git
synced 2024-12-21 08:42:39 +03:00
PM-15177: Improve destructive fallback logic (#4372)
This commit is contained in:
parent
366c86da41
commit
019bf8d0fa
11 changed files with 104 additions and 242 deletions
|
@ -37,7 +37,6 @@ import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.Clock
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,7 +73,6 @@ object PlatformDiskModule {
|
||||||
fun provideEventDatabase(
|
fun provideEventDatabase(
|
||||||
app: Application,
|
app: Application,
|
||||||
databaseSchemeManager: DatabaseSchemeManager,
|
databaseSchemeManager: DatabaseSchemeManager,
|
||||||
clock: Clock,
|
|
||||||
): PlatformDatabase =
|
): PlatformDatabase =
|
||||||
Room
|
Room
|
||||||
.databaseBuilder(
|
.databaseBuilder(
|
||||||
|
@ -84,12 +82,7 @@ object PlatformDiskModule {
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.addTypeConverter(ZonedDateTimeTypeConverter())
|
.addTypeConverter(ZonedDateTimeTypeConverter())
|
||||||
.addCallback(
|
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
|
||||||
DatabaseSchemeCallback(
|
|
||||||
databaseSchemeManager = databaseSchemeManager,
|
|
||||||
clock = clock,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
package com.x8bit.bitwarden.data.platform.manager
|
package com.x8bit.bitwarden.data.platform.manager
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager for tracking changes to database scheme(s).
|
* Manager for tracking changes to database scheme(s).
|
||||||
*/
|
*/
|
||||||
interface DatabaseSchemeManager {
|
interface DatabaseSchemeManager {
|
||||||
|
/**
|
||||||
|
* Clears the sync state for all users and emits on the [databaseSchemeChangeFlow].
|
||||||
|
*/
|
||||||
|
fun clearSyncState()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The instant of the last database schema change performed on the database, if any.
|
* Emits whenever the sync state hs been cleared.
|
||||||
*
|
|
||||||
* There is only a single scheme change instant tracked for all database schemes. It is expected
|
|
||||||
* that a scheme change to any database will update this value and trigger a sync.
|
|
||||||
*/
|
*/
|
||||||
var lastDatabaseSchemeChangeInstant: Instant?
|
val databaseSchemeChangeFlow: Flow<Unit>
|
||||||
|
|
||||||
/**
|
|
||||||
* A flow of the last database schema change instant.
|
|
||||||
*/
|
|
||||||
val lastDatabaseSchemeChangeInstantFlow: Flow<Instant?>
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,27 @@
|
||||||
package com.x8bit.bitwarden.data.platform.manager
|
package com.x8bit.bitwarden.data.platform.manager
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||||
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 com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Primary implementation of [DatabaseSchemeManager].
|
* Primary implementation of [DatabaseSchemeManager].
|
||||||
*/
|
*/
|
||||||
class DatabaseSchemeManagerImpl(
|
class DatabaseSchemeManagerImpl(
|
||||||
|
val authDiskSource: AuthDiskSource,
|
||||||
val settingsDiskSource: SettingsDiskSource,
|
val settingsDiskSource: SettingsDiskSource,
|
||||||
val dispatcherManager: DispatcherManager,
|
|
||||||
) : DatabaseSchemeManager {
|
) : DatabaseSchemeManager {
|
||||||
|
private val mutableSharedFlow: MutableSharedFlow<Unit> = bufferedMutableSharedFlow()
|
||||||
|
|
||||||
private val unconfinedScope = CoroutineScope(dispatcherManager.unconfined)
|
override fun clearSyncState() {
|
||||||
|
authDiskSource.userState?.accounts?.forEach { (userId, _) ->
|
||||||
override var lastDatabaseSchemeChangeInstant: Instant?
|
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = null)
|
||||||
get() = settingsDiskSource.lastDatabaseSchemeChangeInstant
|
|
||||||
set(value) {
|
|
||||||
settingsDiskSource.lastDatabaseSchemeChangeInstant = value
|
|
||||||
}
|
}
|
||||||
|
mutableSharedFlow.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
override val lastDatabaseSchemeChangeInstantFlow =
|
override val databaseSchemeChangeFlow: Flow<Unit> = mutableSharedFlow.asSharedFlow()
|
||||||
settingsDiskSource
|
|
||||||
.lastDatabaseSchemeChangeInstantFlow
|
|
||||||
.stateIn(
|
|
||||||
scope = unconfinedScope,
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
initialValue = settingsDiskSource.lastDatabaseSchemeChangeInstant,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -307,10 +307,10 @@ object PlatformManagerModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDatabaseSchemeManager(
|
fun provideDatabaseSchemeManager(
|
||||||
|
authDiskSource: AuthDiskSource,
|
||||||
settingsDiskSource: SettingsDiskSource,
|
settingsDiskSource: SettingsDiskSource,
|
||||||
dispatcherManager: DispatcherManager,
|
|
||||||
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
|
): DatabaseSchemeManager = DatabaseSchemeManagerImpl(
|
||||||
|
authDiskSource = authDiskSource,
|
||||||
settingsDiskSource = settingsDiskSource,
|
settingsDiskSource = settingsDiskSource,
|
||||||
dispatcherManager = dispatcherManager,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.Clock
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,7 +50,6 @@ object GeneratorDiskModule {
|
||||||
fun providePasswordHistoryDatabase(
|
fun providePasswordHistoryDatabase(
|
||||||
app: Application,
|
app: Application,
|
||||||
databaseSchemeManager: DatabaseSchemeManager,
|
databaseSchemeManager: DatabaseSchemeManager,
|
||||||
clock: Clock,
|
|
||||||
): PasswordHistoryDatabase {
|
): PasswordHistoryDatabase {
|
||||||
return Room
|
return Room
|
||||||
.databaseBuilder(
|
.databaseBuilder(
|
||||||
|
@ -59,12 +57,7 @@ object GeneratorDiskModule {
|
||||||
klass = PasswordHistoryDatabase::class.java,
|
klass = PasswordHistoryDatabase::class.java,
|
||||||
name = "passcode_history_database",
|
name = "passcode_history_database",
|
||||||
)
|
)
|
||||||
.addCallback(
|
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
|
||||||
DatabaseSchemeCallback(
|
|
||||||
databaseSchemeManager = databaseSchemeManager,
|
|
||||||
clock = clock,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,14 @@ package com.x8bit.bitwarden.data.vault.datasource.disk.callback
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
import com.x8bit.bitwarden.data.platform.manager.DatabaseSchemeManager
|
||||||
import java.time.Clock
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [RoomDatabase.Callback] for tracking database scheme changes.
|
* A [RoomDatabase.Callback] for tracking database scheme changes.
|
||||||
*/
|
*/
|
||||||
class DatabaseSchemeCallback(
|
class DatabaseSchemeCallback(
|
||||||
private val databaseSchemeManager: DatabaseSchemeManager,
|
private val databaseSchemeManager: DatabaseSchemeManager,
|
||||||
private val clock: Clock,
|
|
||||||
) : RoomDatabase.Callback() {
|
) : RoomDatabase.Callback() {
|
||||||
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant = clock.instant()
|
databaseSchemeManager.clearSyncState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.time.Clock
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,7 +33,6 @@ class VaultDiskModule {
|
||||||
fun provideVaultDatabase(
|
fun provideVaultDatabase(
|
||||||
app: Application,
|
app: Application,
|
||||||
databaseSchemeManager: DatabaseSchemeManager,
|
databaseSchemeManager: DatabaseSchemeManager,
|
||||||
clock: Clock,
|
|
||||||
): VaultDatabase =
|
): VaultDatabase =
|
||||||
Room
|
Room
|
||||||
.databaseBuilder(
|
.databaseBuilder(
|
||||||
|
@ -43,7 +41,7 @@ class VaultDiskModule {
|
||||||
name = "vault_database",
|
name = "vault_database",
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.addCallback(DatabaseSchemeCallback(databaseSchemeManager, clock))
|
.addCallback(DatabaseSchemeCallback(databaseSchemeManager = databaseSchemeManager))
|
||||||
.addTypeConverter(ZonedDateTimeTypeConverter())
|
.addTypeConverter(ZonedDateTimeTypeConverter())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,6 @@ 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
|
||||||
|
@ -326,9 +325,8 @@ class VaultRepositoryImpl(
|
||||||
.launchIn(ioScope)
|
.launchIn(ioScope)
|
||||||
|
|
||||||
databaseSchemeManager
|
databaseSchemeManager
|
||||||
.lastDatabaseSchemeChangeInstantFlow
|
.databaseSchemeChangeFlow
|
||||||
.filterNotNull()
|
.onEach { sync(forced = true) }
|
||||||
.onEach { sync() }
|
|
||||||
.launchIn(ioScope)
|
.launchIn(ioScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,13 +359,11 @@ class VaultRepositoryImpl(
|
||||||
val userId = activeUserId ?: return
|
val userId = activeUserId ?: return
|
||||||
val currentInstant = clock.instant()
|
val currentInstant = clock.instant()
|
||||||
val lastSyncInstant = settingsDiskSource.getLastSyncTime(userId = userId)
|
val lastSyncInstant = settingsDiskSource.getLastSyncTime(userId = userId)
|
||||||
val lastDatabaseSchemeChangeInstant = databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
|
||||||
|
|
||||||
// Sync if we have never done so, the last time was at last 30 minutes ago, or the database
|
// Sync if we have never done so, the last time was at last 30 minutes ago, or the database
|
||||||
// scheme changed since the last sync.
|
// scheme changed since the last sync.
|
||||||
if (lastSyncInstant == null ||
|
if (lastSyncInstant == null ||
|
||||||
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES)) ||
|
currentInstant.isAfter(lastSyncInstant.plus(30, ChronoUnit.MINUTES))
|
||||||
lastDatabaseSchemeChangeInstant?.isAfter(lastSyncInstant) == true
|
|
||||||
) {
|
) {
|
||||||
sync()
|
sync()
|
||||||
}
|
}
|
||||||
|
@ -1347,37 +1343,33 @@ class VaultRepositoryImpl(
|
||||||
val lastSyncInstant = settingsDiskSource
|
val lastSyncInstant = settingsDiskSource
|
||||||
.getLastSyncTime(userId = userId)
|
.getLastSyncTime(userId = userId)
|
||||||
?.toEpochMilli()
|
?.toEpochMilli()
|
||||||
?: 0
|
lastSyncInstant?.let { lastSyncTimeMs ->
|
||||||
val lastDatabaseSchemeChangeInstant = databaseSchemeManager
|
// If the lasSyncState is null we just sync, no checks required.
|
||||||
.lastDatabaseSchemeChangeInstant
|
syncService
|
||||||
?.toEpochMilli()
|
.getAccountRevisionDateMillis()
|
||||||
?: 0
|
.fold(
|
||||||
syncService
|
onSuccess = { serverRevisionDate ->
|
||||||
.getAccountRevisionDateMillis()
|
if (serverRevisionDate < lastSyncTimeMs) {
|
||||||
.fold(
|
// We can skip the actual sync call if there is no new data or
|
||||||
onSuccess = { serverRevisionDate ->
|
// database scheme changes since the last sync.
|
||||||
if (serverRevisionDate < lastSyncInstant &&
|
vaultDiskSource.resyncVaultData(userId = userId)
|
||||||
lastDatabaseSchemeChangeInstant < lastSyncInstant
|
settingsDiskSource.storeLastSyncTime(
|
||||||
) {
|
userId = userId,
|
||||||
// We can skip the actual sync call if there is no new data or database
|
lastSyncTime = clock.instant(),
|
||||||
// scheme changes since the last sync.
|
)
|
||||||
vaultDiskSource.resyncVaultData(userId = userId)
|
val itemsAvailable = vaultDiskSource
|
||||||
settingsDiskSource.storeLastSyncTime(
|
.getCiphers(userId)
|
||||||
userId = userId,
|
.firstOrNull()
|
||||||
lastSyncTime = clock.instant(),
|
?.isNotEmpty() == true
|
||||||
)
|
return SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
||||||
val itemsAvailable = vaultDiskSource
|
}
|
||||||
.getCiphers(userId)
|
},
|
||||||
.firstOrNull()
|
onFailure = {
|
||||||
?.isNotEmpty() == true
|
updateVaultStateFlowsToError(throwable = it)
|
||||||
return SyncVaultDataResult.Success(itemsAvailable = itemsAvailable)
|
return SyncVaultDataResult.Error(throwable = it)
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
onFailure = {
|
}
|
||||||
updateVaultStateFlowsToError(throwable = it)
|
|
||||||
return SyncVaultDataResult.Error(throwable = it)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return syncService
|
return syncService
|
||||||
|
|
|
@ -1,74 +1,62 @@
|
||||||
package com.x8bit.bitwarden.data.platform.manager
|
package com.x8bit.bitwarden.data.platform.manager
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
|
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
|
||||||
import io.mockk.every
|
import com.x8bit.bitwarden.data.platform.datasource.disk.util.FakeSettingsDiskSource
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
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 fakeAuthDiskSource = FakeAuthDiskSource()
|
||||||
private val mockSettingsDiskSource: SettingsDiskSource = mockk {
|
private val fakeSettingsDiskSource = FakeSettingsDiskSource()
|
||||||
every {
|
|
||||||
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,
|
authDiskSource = fakeAuthDiskSource,
|
||||||
dispatcherManager = dispatcherManager,
|
settingsDiskSource = fakeSettingsDiskSource,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@BeforeEach
|
||||||
@Test
|
fun setup() {
|
||||||
fun `setLastDatabaseSchemeChangeInstant persists value in settingsDiskSource`() {
|
fakeAuthDiskSource.userState = USER_STATE
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant = FIXED_CLOCK.instant()
|
fakeSettingsDiskSource.storeLastSyncTime(USER_ID_1, FIXED_CLOCK.instant())
|
||||||
verify {
|
fakeSettingsDiskSource.storeLastSyncTime(USER_ID_2, FIXED_CLOCK.instant())
|
||||||
mockSettingsDiskSource.lastDatabaseSchemeChangeInstant = FIXED_CLOCK.instant()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `setLastDatabaseSchemeChangeInstant does emit value`() = runTest {
|
fun `clearSyncState clears lastSyncTimes and emit`() = runTest {
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstantFlow.test {
|
assertNotNull(fakeSettingsDiskSource.getLastSyncTime(USER_ID_1))
|
||||||
// Assert the value is initialized to null
|
assertNotNull(fakeSettingsDiskSource.getLastSyncTime(USER_ID_2))
|
||||||
assertEquals(
|
|
||||||
null,
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
// Assert the new value is emitted
|
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant = FIXED_CLOCK.instant()
|
|
||||||
assertEquals(
|
|
||||||
FIXED_CLOCK.instant(),
|
|
||||||
awaitItem(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
databaseSchemeManager.databaseSchemeChangeFlow.test {
|
||||||
fun `getLastDatabaseSchemeChangeInstant retrieves stored value from settingsDiskSource`() {
|
databaseSchemeManager.clearSyncState()
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
awaitItem()
|
||||||
verify {
|
expectNoEvents()
|
||||||
mockSettingsDiskSource.lastDatabaseSchemeChangeInstant
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertNull(fakeSettingsDiskSource.getLastSyncTime(USER_ID_1))
|
||||||
|
assertNull(fakeSettingsDiskSource.getLastSyncTime(USER_ID_2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val USER_ID_1: String = "USER_ID_1"
|
||||||
|
private const val USER_ID_2: String = "USER_ID_2"
|
||||||
|
|
||||||
|
private val USER_STATE: UserStateJson = UserStateJson(
|
||||||
|
activeUserId = USER_ID_1,
|
||||||
|
accounts = mapOf(
|
||||||
|
USER_ID_1 to mockk(),
|
||||||
|
USER_ID_2 to mockk(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
private val FIXED_CLOCK: Clock = Clock.fixed(
|
||||||
Instant.parse("2023-10-27T12:00:00Z"),
|
Instant.parse("2023-10-27T12:00:00Z"),
|
||||||
ZoneOffset.UTC,
|
ZoneOffset.UTC,
|
||||||
|
|
|
@ -7,26 +7,17 @@ import io.mockk.mockk
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.time.Clock
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZoneOffset
|
|
||||||
|
|
||||||
class DatabaseSchemeCallbackTest {
|
class DatabaseSchemeCallbackTest {
|
||||||
|
|
||||||
private val databaseSchemeManager: DatabaseSchemeManager = mockk {
|
private val databaseSchemeManager: DatabaseSchemeManager = mockk {
|
||||||
every { lastDatabaseSchemeChangeInstant = any() } just runs
|
every { clearSyncState() } just runs
|
||||||
}
|
}
|
||||||
private val callback = DatabaseSchemeCallback(databaseSchemeManager, FIXED_CLOCK)
|
private val callback = DatabaseSchemeCallback(databaseSchemeManager)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `onDestructiveMigration updates lastDatabaseSchemeChangeInstant`() {
|
fun `onDestructiveMigration calls clearSyncState`() {
|
||||||
callback.onDestructiveMigration(mockk())
|
callback.onDestructiveMigration(mockk())
|
||||||
|
verify(exactly = 1) { databaseSchemeManager.clearSyncState() }
|
||||||
verify { databaseSchemeManager.lastDatabaseSchemeChangeInstant = FIXED_CLOCK.instant() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val FIXED_CLOCK: Clock = Clock.fixed(
|
|
||||||
Instant.parse("2023-10-27T12:00:00Z"),
|
|
||||||
ZoneOffset.UTC,
|
|
||||||
)
|
|
||||||
|
|
|
@ -193,14 +193,9 @@ class VaultRepositoryTest {
|
||||||
mutableUnlockedUserIdsStateFlow.first { userId in it }
|
mutableUnlockedUserIdsStateFlow.first { userId in it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val mutableLastDatabaseSchemeChangeInstantFlow = MutableStateFlow<Instant?>(null)
|
private val mutableDatabaseSchemeChangeFlow = bufferedMutableSharedFlow<Unit>()
|
||||||
private val databaseSchemeManager: DatabaseSchemeManager = mockk {
|
private val databaseSchemeManager: DatabaseSchemeManager = mockk {
|
||||||
every {
|
every { databaseSchemeChangeFlow } returns mutableDatabaseSchemeChangeFlow
|
||||||
lastDatabaseSchemeChangeInstant
|
|
||||||
} returns mutableLastDatabaseSchemeChangeInstantFlow.value
|
|
||||||
every {
|
|
||||||
lastDatabaseSchemeChangeInstantFlow
|
|
||||||
} returns mutableLastDatabaseSchemeChangeInstantFlow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
|
private val mutableFullSyncFlow = bufferedMutableSharedFlow<Unit>()
|
||||||
|
@ -787,34 +782,14 @@ class VaultRepositoryTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `lastDatabaseSchemeChangeInstantFlow should trigger sync when new value is not null`() =
|
fun `databaseSchemeChangeFlow should trigger sync on emission`() = runTest {
|
||||||
runTest {
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
coEvery { syncService.sync() } just awaits
|
||||||
every {
|
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
|
||||||
} returns mutableLastDatabaseSchemeChangeInstantFlow.value
|
|
||||||
coEvery { syncService.sync() } just awaits
|
|
||||||
|
|
||||||
mutableLastDatabaseSchemeChangeInstantFlow.value = clock.instant()
|
mutableDatabaseSchemeChangeFlow.tryEmit(Unit)
|
||||||
|
|
||||||
coVerify(exactly = 1) { syncService.sync() }
|
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() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sync with forced should skip checks and call the syncService sync`() {
|
fun `sync with forced should skip checks and call the syncService sync`() {
|
||||||
|
@ -1123,60 +1098,6 @@ class VaultRepositoryTest {
|
||||||
coVerify(exactly = 0) { syncService.sync() }
|
coVerify(exactly = 0) { syncService.sync() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `syncIfNecessary when there is no last scheme change should not sync the vault`() {
|
|
||||||
val userId = "mockId-1"
|
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
|
||||||
every {
|
|
||||||
settingsDiskSource.getLastSyncTime(userId)
|
|
||||||
} returns clock.instant().minus(1, ChronoUnit.MINUTES)
|
|
||||||
every {
|
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
|
||||||
} returns null
|
|
||||||
coEvery { syncService.sync() } just awaits
|
|
||||||
|
|
||||||
vaultRepository.syncIfNecessary()
|
|
||||||
|
|
||||||
coVerify(exactly = 0) { syncService.sync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
@Test
|
|
||||||
fun `syncIfNecessary when the last scheme change is before the last sync time should not sync the vault`() {
|
|
||||||
val userId = "mockId-1"
|
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
|
||||||
every {
|
|
||||||
settingsDiskSource.getLastSyncTime(userId)
|
|
||||||
} returns clock.instant().plus(1, ChronoUnit.MINUTES)
|
|
||||||
every {
|
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
|
||||||
} returns clock.instant().minus(1, ChronoUnit.MINUTES)
|
|
||||||
|
|
||||||
coEvery { syncService.sync() } just awaits
|
|
||||||
|
|
||||||
vaultRepository.syncIfNecessary()
|
|
||||||
|
|
||||||
coVerify(exactly = 0) { syncService.sync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
@Test
|
|
||||||
fun `syncIfNecessary when the last scheme change is after the last sync time should sync the vault`() {
|
|
||||||
val userId = "mockId-1"
|
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
|
||||||
every {
|
|
||||||
settingsDiskSource.getLastSyncTime(userId)
|
|
||||||
} returns clock.instant().minus(1, ChronoUnit.MINUTES)
|
|
||||||
every {
|
|
||||||
databaseSchemeManager.lastDatabaseSchemeChangeInstant
|
|
||||||
} returns clock.instant().plus(1, ChronoUnit.MINUTES)
|
|
||||||
coEvery { syncService.sync() } just awaits
|
|
||||||
|
|
||||||
vaultRepository.syncIfNecessary()
|
|
||||||
|
|
||||||
coVerify { syncService.sync() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sync when the last sync time is older than the revision date should sync the vault`() {
|
fun `sync when the last sync time is older than the revision date should sync the vault`() {
|
||||||
val userId = "mockId-1"
|
val userId = "mockId-1"
|
||||||
|
|
Loading…
Reference in a new issue