package com.peachpasswords.vault import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test /** * Integration coverage for the full create → confirm → seal → unlock recovery * chain (brief Fix 1). The UI confirmation step ([RecoveryWordsViewModel.confirmAndComplete]) * does exactly one thing the repository can be tested against: it derives the * vault's recovery words from the sync root key and calls [VaultRepository.setRecoveryWords]. * These tests drive that same call so the whole escrow-sealing path is exercised * without a Compose harness. */ class RecoveryEscrowIntegrationTest { private class FakeStorage : VaultStorage { var slotA: StoredVault? = null var slotB: String? = null private var ver: Long = 0 private var settings = VaultSettings() override fun loadSlotA() = slotA override fun hasVault() = slotA != null override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true } override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true } var decoy: StoredVault? = null override fun loadDecoySlot() = decoy override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true } override fun hasDecoySlot() = decoy != null override fun clearDecoySlot() { decoy = null } override fun version() = ver override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver } override fun loadSettings() = settings override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true } override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); return true } } /** The 12 recovery words this vault would actually display, as the confirm screen derives them. */ private fun recoveryWordsOf(repo: VaultRepository): List { val key = repo.syncMasterKey()!! return try { rootKeyToWords(key) } finally { VaultCrypto.zero(key) } } private fun login(title: String, password: String = "pw") = VaultEntry(id = "", title = title, type = "login", password = password, createdAt = 0, modifiedAt = 0) /** create vault → confirm words → escrow is sealed at rest. */ @Test fun vault_creation_seals_recovery_escrow() = runBlocking { val storage = FakeStorage() val repo = VaultRepository(storage) repo.createVault("correct horse battery") // Pre-confirmation: nothing sealed yet. assertFalse(repo.hasRecoveryEscrow()) // The confirmation step seals both escrows. assertTrue(repo.setRecoveryWords(recoveryWordsOf(repo))) assertTrue(repo.hasRecoveryEscrow()) } /** Full round-trip: create → confirm → lock → unlock with recovery words → success. */ @Test fun recovery_words_unlock_after_creation() = runBlocking { val storage = FakeStorage() val repo = VaultRepository(storage) repo.createVault("correct horse battery") repo.addEntry(login("GitHub")) val words = recoveryWordsOf(repo) repo.setRecoveryWords(words) repo.lock() assertTrue(repo.unlockWithRecoveryWords(words)) assertTrue(repo.getEntries().any { it.title == "GitHub" }) } /** A vault whose confirmation step never ran exposes no escrow → "recovery not available". */ @Test fun recovery_not_available_before_confirmation() = runBlocking { val storage = FakeStorage() val repo = VaultRepository(storage) repo.createVault("correct horse battery") val words = recoveryWordsOf(repo) // derived, but never confirmed repo.lock() assertFalse(repo.hasRecoveryEscrow()) assertFalse(repo.unlockWithRecoveryWords(words)) } /** The original recovery words keep working after the master password changes. */ @Test fun recovery_survives_password_change() = runBlocking { val storage = FakeStorage() val repo = VaultRepository(storage) repo.createVault("first-master-pass") repo.addEntry(login("Bank")) val words = recoveryWordsOf(repo) repo.setRecoveryWords(words) assertTrue(repo.changePassword("first-master-pass", "second-master-pass")) repo.lock() // Recover with the ORIGINAL words, then set a third password and unlock with it. assertTrue(repo.unlockWithRecoveryWords(words)) assertTrue(repo.resetPasswordWithRecoveryWords(words, "third-master-pass")) repo.lock() assertEquals(UnlockResult.Success, repo.unlock("third-master-pass")) assertTrue(repo.getEntries().any { it.title == "Bank" }) } }