PM-15177: Improve destructive fallback logic (#4372)

This commit is contained in:
David Perez 2024-11-22 15:59:40 -06:00 committed by GitHub
parent 366c86da41
commit 019bf8d0fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 104 additions and 242 deletions

View file

@ -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

View file

@ -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?>
} }

View file

@ -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,
)
} }

View file

@ -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,
) )
} }

View file

@ -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()
} }

View file

@ -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()
} }
} }

View file

@ -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()

View file

@ -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

View file

@ -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,

View file

@ -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,
)

View file

@ -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"