package com.peachpasswords.sync import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test class PairedDeviceRevocationTest { private val browser = PairedDevice( deviceId = "browser-1", deviceName = "Chrome", deviceType = "extension", longTermPublicKeyHex = "ab".repeat(32), pairedAt = 1L, ) @Test fun `new revocation records the key and removes the paired row`() { val mutation = planDeviceRevocation( devices = listOf(browser), revocations = emptyList(), deviceId = browser.deviceId, revokedByDeviceId = "phone-1", nowMs = 42L, ) assertEquals(emptyList(), mutation.devices) assertEquals(1, mutation.revocations.size) assertEquals(browser.longTermPublicKeyHex, mutation.added?.longTermPublicKeyHex) assertEquals(42L, mutation.added?.revokedAt) } @Test fun `full ledger keeps terminal records during peer merge capacity`() { val existing = List(500) { index -> DeviceRevocation("terminal-$index", index.toString(16).padStart(64, '0'), index.toLong(), "issuer-a") } val merged = mergeRevocationRecords(existing, listOf( DeviceRevocation("peer-extra", "ff".repeat(32), 501L, "issuer-b"), )) assertEquals(500, merged.records.size) assertEquals(0, merged.admitted.size) assertEquals("terminal-0", merged.records.first().deviceId) } @Test fun `same issuer local flood uses bounded capacity`() { var records = emptyList() repeat(501) { index -> records = if (records.size < 500) records + DeviceRevocation("local-$index", index.toString(16).padStart(64, '0'), index.toLong(), "issuer-a") else records.drop(1) + DeviceRevocation("local-$index", index.toString(16).padStart(64, '0'), index.toLong(), "issuer-a") } assertEquals(500, records.size) assertEquals("local-1", records.first().deviceId) assertEquals("local-500", records.last().deviceId) } @Test fun `different issuer local capacity preserves genuine issuer record`() { val existing = List(500) { index -> DeviceRevocation("terminal-$index", index.toString(16).padStart(64, '0'), index.toLong(), if (index == 0) "issuer-a" else "issuer-b") } val removable = existing.indexOfFirst { it.revokedByDeviceId != "issuer-a" } val records = existing.toMutableList().also { it.removeAt(removable); it += DeviceRevocation("local-new", "ff".repeat(32), 501L, "issuer-a") } assertEquals(500, records.size) assertEquals("terminal-0", records.first().deviceId) assertEquals("local-new", records.last().deviceId) } @Test fun `existing revocation still removes a stale overlapping paired row`() { val existing = DeviceRevocation( deviceId = browser.deviceId, longTermPublicKeyHex = browser.longTermPublicKeyHex!!, revokedAt = 1L, revokedByDeviceId = "phone-1", ) val mutation = planDeviceRevocation( devices = listOf(browser), revocations = listOf(existing), deviceId = browser.deviceId, revokedByDeviceId = "phone-1", nowMs = 99L, ) assertEquals(emptyList(), mutation.devices) assertEquals(listOf(existing), mutation.revocations) assertNull(mutation.added) } }