VaultLockTimerTest.kt

4.0 KB · raw

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.Test1112/** In-memory [VaultStorage] so the timer can drive a real repository off-device. */13private 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}3334/**35 * Drives [VaultLockTimer] on virtual time. The timer's coroutine scope is replaced36 * with one backed by the test scheduler, so [advanceTimeBy] moves the idle clock37 * deterministically without real waiting.38 */39@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() // 50s in — restart the 60s window66        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}