1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertNotNull6import org.junit.Assert.assertNull7import org.junit.Test89class PasskeyConsolidationTest {1011 private class Storage : VaultStorage {12 var slotA: StoredVault? = null13 var slotB: String? = null14 private var ver: Long = 015 private var settings = VaultSettings()16 override fun loadSlotA() = slotA17 override fun hasVault() = slotA != null18 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }19 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }20 var decoy: StoredVault? = null21 override fun loadDecoySlot() = decoy22 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }23 override fun hasDecoySlot() = decoy != null24 override fun clearDecoySlot() { decoy = null }25 override fun version() = ver26 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }27 override fun loadSettings() = settings28 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }29 override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); return true }30 }3132 private fun repo() = VaultRepository(Storage())3334 @Test fun passkey_only_entry_merges_into_matching_login_on_unlock() = runBlocking {35 val vault = repo()36 vault.createVault("test-password-123")3738 vault.addEntry(VaultEntry(39 id = "", type = "login", title = "Example",40 username = "june", password = "hunter2",41 url = "https://example.com/login",42 createdAt = 0, modifiedAt = 0,43 ))44 vault.addEntry(VaultEntry(45 id = "", type = "login", title = "example.com passkey",46 url = "https://example.com",47 rpId = "example.com", credentialId = "Y3JlZA",48 privateKeyJwk = """{"kty":"EC","crv":"P-256","d":"dGVzdA"}""",49 algorithm = -7, signCount = 5,50 createdAt = 0, modifiedAt = 0,51 ))5253 assertEquals(2, vault.getEntries().size)5455 vault.lock()56 vault.unlock("test-password-123")5758 59 val active = vault.getEntries()60 assertEquals("orphan merged and trashed — one active login", 1, active.size)6162 val merged = active[0]63 assertEquals("Example", merged.title)64 assertEquals("hunter2", merged.password)65 assertEquals("example.com", merged.rpId)66 assertEquals("Y3JlZA", merged.credentialId)67 assertEquals(-7, merged.algorithm)68 assertEquals(5, merged.signCount)69 }7071 @Test fun no_consolidation_when_login_already_has_passkey() = runBlocking {72 val vault = repo()73 vault.createVault("test-password-123")7475 vault.addEntry(VaultEntry(76 id = "", type = "login", title = "Example",77 username = "june", password = "hunter2",78 url = "https://example.com",79 rpId = "example.com", credentialId = "ZXhpc3Rpbmc",80 privateKeyJwk = """{"kty":"EC","crv":"P-256","d":"b2xk"}""",81 algorithm = -7,82 createdAt = 0, modifiedAt = 0,83 ))84 vault.addEntry(VaultEntry(85 id = "", type = "login", title = "orphan",86 url = "https://example.com",87 rpId = "example.com", credentialId = "b3JwaGFu",88 privateKeyJwk = """{"kty":"EC","crv":"P-256","d":"bmV3"}""",89 algorithm = -7,90 createdAt = 0, modifiedAt = 0,91 ))9293 vault.lock()94 vault.unlock("test-password-123")9596 assertEquals("both stay — target already has a passkey", 2, vault.getEntries().size)97 }9899 @Test fun no_consolidation_for_different_domains() = runBlocking {100 val vault = repo()101 vault.createVault("test-password-123")102103 vault.addEntry(VaultEntry(104 id = "", type = "login", title = "Other",105 username = "june", password = "hunter2",106 url = "https://other.com",107 createdAt = 0, modifiedAt = 0,108 ))109 vault.addEntry(VaultEntry(110 id = "", type = "login", title = "example passkey",111 rpId = "example.com", credentialId = "Y3JlZA",112 privateKeyJwk = """{"kty":"EC","crv":"P-256","d":"dGVzdA"}""",113 algorithm = -7,114 createdAt = 0, modifiedAt = 0,115 ))116117 vault.lock()118 vault.unlock("test-password-123")119120 assertEquals("no domain match — both stay", 2, vault.getEntries().size)121 }122}