1package com.peachpasswords.vault23import kotlinx.coroutines.CoroutineScope4import kotlinx.coroutines.test.StandardTestDispatcher5import kotlinx.coroutines.test.advanceTimeBy6import kotlinx.coroutines.test.runCurrent7import kotlinx.coroutines.test.runTest8import org.junit.Assert.assertFalse9import org.junit.Assert.assertTrue10import org.junit.Test111213private class LockTimerFakeStorage : VaultStorage {14 private var slotA: StoredVault? = null15 private var slotB: String? = null16 private var ver: Long = 017 private var settings = VaultSettings()18 override fun loadSlotA() = slotA19 override fun hasVault() = slotA != null20 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }21 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }22 private var decoy: StoredVault? = null23 override fun loadDecoySlot() = decoy24 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }25 override fun hasDecoySlot() = decoy != null26 override fun clearDecoySlot() { decoy = null }27 override fun version() = ver28 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }29 override fun loadSettings() = settings30 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }31 override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); return true }32}33343536373839@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)40class VaultLockTimerTest {4142 private suspend fun unlockedRepo(autoLockMinutes: Int?): VaultRepository {43 val repo = VaultRepository(LockTimerFakeStorage())44 repo.createVault("correct horse battery staple")45 repo.updateSettings(VaultSettings(autoLockMinutes = autoLockMinutes))46 assertTrue(repo.isUnlocked.value)47 return repo48 }4950 @Test fun timer_locks_after_configured_delay() = runTest {51 val repo = unlockedRepo(autoLockMinutes = 1)52 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }53 timer.start()54 advanceTimeBy(59_000); runCurrent()55 assertTrue("still unlocked before the window elapses", repo.isUnlocked.value)56 advanceTimeBy(2_000); runCurrent()57 assertFalse("locked once the idle window elapses", repo.isUnlocked.value)58 }5960 @Test fun poke_resets_the_countdown() = runTest {61 val repo = unlockedRepo(autoLockMinutes = 1)62 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }63 timer.start()64 advanceTimeBy(50_000); runCurrent()65 timer.poke() 66 advanceTimeBy(50_000); runCurrent()67 assertTrue("poke pushed the lock past the original deadline", repo.isUnlocked.value)68 advanceTimeBy(11_000); runCurrent()69 assertFalse("locks 60s after the last poke", repo.isUnlocked.value)70 }7172 @Test fun cancel_prevents_firing() = runTest {73 val repo = unlockedRepo(autoLockMinutes = 1)74 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }75 timer.start()76 timer.cancel()77 advanceTimeBy(120_000); runCurrent()78 assertTrue("cancelled timer never locks", repo.isUnlocked.value)79 }8081 @Test fun null_auto_lock_never_fires() = runTest {82 val repo = unlockedRepo(autoLockMinutes = null)83 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }84 timer.start()85 advanceTimeBy(3_600_000); runCurrent()86 assertTrue("null autoLockMinutes means never lock", repo.isUnlocked.value)87 }88}