SlotAUnlockTest.kt

4.9 KB · raw

1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertNotNull7import org.junit.Assert.assertNull8import org.junit.Assert.assertTrue9import org.junit.Test1011/** In-memory [VaultStorage] for the non-committing dual-derivation path (MF-5). */12private class SlotAFakeStorage : VaultStorage {13    private var slotA: StoredVault? = null14    private var slotB: String? = null15    private var ver: Long = 016    private var settings = VaultSettings()17    override fun loadSlotA() = slotA18    override fun hasVault() = slotA != null19    override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }20    override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }21    private var decoy: StoredVault? = null22    override fun loadDecoySlot() = decoy23    override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }24    override fun hasDecoySlot() = decoy != null25    override fun clearDecoySlot() { decoy = null }26    override fun version() = ver27    override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }28    override fun loadSettings() = settings29    override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }30    override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); return true }31}3233class SlotAUnlockTest {34    private fun freshRepo(): VaultRepository = VaultRepository(SlotAFakeStorage())3536    @Test fun try_unlock_does_not_mutate_session() = runBlocking {37        val repo = freshRepo()38        repo.createVault("correct horse battery staple")39        repo.addEntry(VaultEntry(id = "", title = "GitHub", type = "login", password = "pw", createdAt = 0, modifiedAt = 0))40        repo.lock()41        assertFalse(repo.isUnlocked.value)4243        val derived = repo.tryUnlockSlotA("correct horse battery staple")44        assertNotNull("right password derives a slot-A unlock", derived)45        // Deriving alone must NOT unlock the live session — only commit does.46        assertFalse("tryUnlockSlotA leaves the session locked", repo.isUnlocked.value)4748        assertEquals(UnlockResult.Success, repo.commitSlotAUnlock(derived!!))49        assertTrue("commit adopts the session", repo.isUnlocked.value)50        assertEquals(1, repo.getEntries().size)51    }5253    @Test fun try_unlock_returns_null_for_wrong_password() = runBlocking {54        val repo = freshRepo()55        repo.createVault("the-real-password-here")56        repo.lock()57        assertNull("wrong password derives nothing", repo.tryUnlockSlotA("not-the-password"))58        assertFalse(repo.isUnlocked.value)59    }6061    @Test fun discard_does_not_unlock() = runBlocking {62        val repo = freshRepo()63        repo.createVault("correct horse battery staple")64        repo.lock()65        val derived = repo.tryUnlockSlotA("correct horse battery staple")!!66        repo.discardSlotAUnlock(derived) // zero without adopting67        assertFalse("discarded unlock never adopts the session", repo.isUnlocked.value)68    }6970    @Test fun sibling_tokens_cannot_replace_a_successfully_adopted_session() = runBlocking {71        val repo = freshRepo()72        repo.createVault("correct horse battery staple")73        repo.lock()7475        val first = repo.tryUnlockSlotA("correct horse battery staple")!!76        val sibling = repo.tryUnlockSlotA("correct horse battery staple")!!77        assertEquals(UnlockResult.Success, repo.commitSlotAUnlock(first))78        assertEquals(79            "a token from the consumed epoch must not replace the adopted session",80            UnlockResult.Error("stale_unlock"),81            repo.commitSlotAUnlock(sibling),82        )83        assertTrue(repo.isUnlocked.value)84    }8586    @Test fun an_old_token_cannot_resume_after_lock_and_new_unlock() = runBlocking {87        val repo = freshRepo()88        repo.createVault("correct horse battery staple")89        repo.lock()9091        val old = repo.tryUnlockSlotA("correct horse battery staple")!!92        repo.lock()93        val current = repo.tryUnlockSlotA("correct horse battery staple")!!94        assertEquals(UnlockResult.Success, repo.commitSlotAUnlock(current))95        assertEquals(96            "a paused old unlock must fail closed after lock/re-unlock",97            UnlockResult.Error("stale_unlock"),98            repo.commitSlotAUnlock(old),99        )100        assertTrue(repo.isUnlocked.value)101    }102103    @Test fun a_token_is_one_shot_even_when_the_first_commit_succeeds() = runBlocking {104        val repo = freshRepo()105        repo.createVault("correct horse battery staple")106        repo.lock()107        val token = repo.tryUnlockSlotA("correct horse battery staple")!!108        assertEquals(UnlockResult.Success, repo.commitSlotAUnlock(token))109        assertEquals(UnlockResult.Error("unlock_token_consumed"), repo.commitSlotAUnlock(token))110        assertTrue(repo.isUnlocked.value)111    }112}