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.Test101112private 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 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) 67 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}