1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertNotEquals7import org.junit.Assert.assertTrue8import org.junit.Test91011121314151617class SnapshotRestoreAtomicityTest {1819 private class FaultStorage : VaultStorage {20 var slotA: StoredVault? = null21 var slotB: String? = null22 var failWriteSlotA = false23 var failWriteSlotB = false24 var failStamp = false25 private var ver: Long = 026 private var settings = VaultSettings()27 var history = VaultHistory()28 override fun loadSlotA() = slotA29 override fun hasVault() = slotA != null30 override fun writeSlotA(slot: StoredVault): Boolean {31 if (failWriteSlotA) return false32 slotA = slot; return true33 }34 override fun loadSlotB() = slotB35 override fun writeSlotB(blobBase64: String): Boolean {36 if (failWriteSlotB) return false37 slotB = blobBase64; return true38 }39 var decoy: StoredVault? = null40 override fun loadDecoySlot() = decoy41 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }42 override fun hasDecoySlot() = decoy != null43 override fun clearDecoySlot() { decoy = null }44 override fun version() = ver45 override fun stampLocalMutation(): Long {46 if (failStamp) throw RuntimeException("stamp failed")47 ver = maxOf(System.currentTimeMillis(), ver + 1); return ver48 }49 override fun loadSettings() = settings50 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }51 override fun loadVaultHistory() = history52 override fun writeVaultHistory(history: VaultHistory): Boolean { this.history = history; return true }53 override fun clearVaultHistory() { history = VaultHistory() }54 override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); history = VaultHistory(); return true }55 }5657 58596061626364 private data class Fixture(65 val repo: VaultRepository,66 val storage: FaultStorage,67 val snapshotId: String,68 )6970 private fun setup(): Fixture = runBlocking {71 val storage = FaultStorage()72 val repo = VaultRepository(storage)73 repo.createVault("pw")74 repo.addEntry(VaultEntry(id = "", title = "First", type = "login", password = "p", createdAt = 0, modifiedAt = 0))75 repo.addEntry(VaultEntry(id = "", title = "Second", type = "login", password = "p", createdAt = 0, modifiedAt = 0))76 val snapshotId = repo.getVaultHistory().first().id77 Fixture(repo, storage, snapshotId)78 }7980 private fun loginEntry(title: String) =81 VaultEntry(id = "", title = title, type = "login", password = "p", createdAt = 0, modifiedAt = 0)8283 @Test fun happy_path_swaps_slots_locks_and_appends_restore_snapshot() = runBlocking {84 val f = setup()85 val slotABefore = f.storage.slotA86 val slotBBefore = f.storage.slotB87 val historyBefore = f.storage.history.snapshots.toList()8889 assertTrue(f.repo.restoreVaultSnapshot(f.snapshotId))9091 92 assertNotEquals(slotABefore, f.storage.slotA)93 assertNotEquals(slotBBefore, f.storage.slotB)94 95 96 assertFalse(f.repo.isUnlocked.value)97 assertEquals(historyBefore.size + 1, f.storage.history.snapshots.size)98 assertEquals("restore", f.storage.history.snapshots.first().reason)99 100 assertEquals(UnlockResult.Success, f.repo.unlock("pw"))101 assertEquals(1, f.repo.getEntries().size)102 assertTrue(f.repo.getEntries().any { it.title == "First" })103 assertFalse(f.repo.getEntries().any { it.title == "Second" })104 }105106 @Test fun write_slot_a_failure_keeps_slots_and_history_unchanged() = runBlocking {107 val f = setup()108 val slotABefore = f.storage.slotA109 val slotBBefore = f.storage.slotB110 val historyBefore = f.storage.history.snapshots.toList()111 f.storage.failWriteSlotA = true112113 assertFalse(f.repo.restoreVaultSnapshot(f.snapshotId))114115 assertEquals(slotABefore, f.storage.slotA)116 assertEquals(slotBBefore, f.storage.slotB)117 assertEquals(historyBefore, f.storage.history.snapshots)118 assertTrue(f.storage.history.snapshots.none { it.reason == "restore" })119 120 assertTrue(f.repo.isUnlocked.value)121 assertEquals(UnlockResult.Success, f.repo.unlock("pw"))122 assertTrue(f.repo.getEntries().any { it.title == "Second" })123 }124125 @Test fun write_slot_b_failure_rolls_back_slot_a_and_history() = runBlocking {126 val f = setup()127 val slotABefore = f.storage.slotA128 val slotBBefore = f.storage.slotB129 val historyBefore = f.storage.history.snapshots.toList()130 f.storage.failWriteSlotB = true131132 assertFalse(f.repo.restoreVaultSnapshot(f.snapshotId))133134 135 136 137 assertEquals(slotABefore, f.storage.slotA)138 assertEquals(slotBBefore, f.storage.slotB)139 assertEquals(historyBefore, f.storage.history.snapshots)140 assertTrue(f.storage.history.snapshots.none { it.reason == "restore" })141 142 assertTrue(f.repo.isUnlocked.value)143 assertEquals(UnlockResult.Success, f.repo.unlock("pw"))144 assertTrue(f.repo.getEntries().any { it.title == "Second" })145 }146147 @Test fun stamp_failure_rolls_back_both_slots_and_history() = runBlocking {148 val f = setup()149 val slotABefore = f.storage.slotA150 val slotBBefore = f.storage.slotB151 val historyBefore = f.storage.history.snapshots.toList()152 f.storage.failStamp = true153154 assertFalse(f.repo.restoreVaultSnapshot(f.snapshotId))155156 157 assertEquals(slotABefore, f.storage.slotA)158 assertEquals(slotBBefore, f.storage.slotB)159 assertEquals(historyBefore, f.storage.history.snapshots)160 assertTrue(f.storage.history.snapshots.none { it.reason == "restore" })161 162 assertEquals(UnlockResult.Success, f.repo.unlock("pw"))163 assertTrue(f.repo.getEntries().any { it.title == "First" })164 assertTrue(f.repo.getEntries().any { it.title == "Second" })165 }166167 @Test fun unknown_snapshot_id_is_a_no_op() = runBlocking {168 val f = setup()169 val slotABefore = f.storage.slotA170 val slotBBefore = f.storage.slotB171 val historyBefore = f.storage.history.snapshots.toList()172 assertTrue(f.repo.isUnlocked.value)173174 assertFalse(f.repo.restoreVaultSnapshot("does-not-exist"))175176 assertEquals(slotABefore, f.storage.slotA)177 assertEquals(slotBBefore, f.storage.slotB)178 assertEquals(historyBefore, f.storage.history.snapshots)179 assertTrue(f.repo.isUnlocked.value)180 }181}