PasskeyConsolidationTest.kt

4.7 KB · raw

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        // getEntries() filters soft-deleted; the orphan is in trash59        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}