package com.peachpasswords.vault import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test /** In-memory [VaultStorage] for exercising last4 derivation on save (MF-3). */ private class CardFakeStorage : VaultStorage { private var slotA: StoredVault? = null private 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 } private 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 } } class CardSerializationTest { private val json = Json { encodeDefaults = true } @Test fun card_network_serializes_as_browser_key() { val card = VaultEntry( id = "c1", type = "card", title = "Visa", cardNumber = "4242424242424242", cardNetwork = "visa", last4 = "4242", createdAt = 0, modifiedAt = 0, ) val encoded = json.encodeToString(VaultEntry.serializer(), card) assertTrue("serializes cardNetwork as \"network\"", encoded.contains("\"network\":\"visa\"")) assertTrue("serializes last4 explicitly", encoded.contains("\"last4\":\"4242\"")) assertFalse("never emits the Android-only key name", encoded.contains("cardNetwork")) } @Test fun network_and_last4_round_trip_from_browser_keys() { val wire = """{"id":"c1","title":"Visa","type":"card","network":"visa","last4":"4242","createdAt":0,"modifiedAt":0}""" val decoded = json.decodeFromString(VaultEntry.serializer(), wire) assertEquals("visa", decoded.cardNetwork) assertEquals("4242", decoded.last4) } @Test fun last4_is_derived_on_save() = runBlocking { val repo = VaultRepository(CardFakeStorage()) repo.createVault("correct horse battery staple") val saved = repo.addEntry( VaultEntry(id = "", type = "card", title = "My Card", cardNumber = "4111 1111 1111 1234", createdAt = 0, modifiedAt = 0), ) assertEquals("derives last4 from the PAN on save", "1234", saved.last4) val updated = repo.updateEntry(saved.copy(cardNumber = "5555 5555 5555 4321")) assertEquals("re-derives last4 on update", "4321", updated.last4) } @Test fun non_card_entry_has_no_last4() = runBlocking { val repo = VaultRepository(CardFakeStorage()) repo.createVault("correct horse battery staple") val saved = repo.addEntry(VaultEntry(id = "", type = "login", title = "GitHub", password = "pw", createdAt = 0, modifiedAt = 0)) assertEquals(null, saved.last4) } }