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() { slotA = null; slotB = null; ver = 0; settings = VaultSettings() }32}33343536373839class VaultLockTimerTest {4041 private suspend fun unlockedRepo(autoLockMinutes: Int?): VaultRepository {42 val repo = VaultRepository(LockTimerFakeStorage())43 repo.createVault("correct horse battery staple")44 repo.updateSettings(VaultSettings(autoLockMinutes = autoLockMinutes))45 assertTrue(repo.isUnlocked.value)46 return repo47 }4849 @Test fun timer_locks_after_configured_delay() = runTest {50 val repo = unlockedRepo(autoLockMinutes = 1)51 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }52 timer.start()53 advanceTimeBy(59_000); runCurrent()54 assertTrue("still unlocked before the window elapses", repo.isUnlocked.value)55 advanceTimeBy(2_000); runCurrent()56 assertFalse("locked once the idle window elapses", repo.isUnlocked.value)57 }5859 @Test fun poke_resets_the_countdown() = runTest {60 val repo = unlockedRepo(autoLockMinutes = 1)61 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }62 timer.start()63 advanceTimeBy(50_000); runCurrent()64 timer.poke() 65 advanceTimeBy(50_000); runCurrent()66 assertTrue("poke pushed the lock past the original deadline", repo.isUnlocked.value)67 advanceTimeBy(11_000); runCurrent()68 assertFalse("locks 60s after the last poke", repo.isUnlocked.value)69 }7071 @Test fun cancel_prevents_firing() = runTest {72 val repo = unlockedRepo(autoLockMinutes = 1)73 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }74 timer.start()75 timer.cancel()76 advanceTimeBy(120_000); runCurrent()77 assertTrue("cancelled timer never locks", repo.isUnlocked.value)78 }7980 @Test fun null_auto_lock_never_fires() = runTest {81 val repo = unlockedRepo(autoLockMinutes = null)82 val timer = VaultLockTimer(repo).also { it.scope = CoroutineScope(StandardTestDispatcher(testScheduler)) }83 timer.start()84 advanceTimeBy(3_600_000); runCurrent()85 assertTrue("null autoLockMinutes means never lock", repo.isUnlocked.value)86 }87}