SyncAlertTest.kt

4.8 KB · raw

1package com.peachpasswords.sync23import com.peachpasswords.vault.Tombstone4import com.peachpasswords.vault.VaultEntry5import org.junit.Assert.assertEquals6import org.junit.Assert.assertNull7import org.junit.Test89class SyncAlertTest {10    @Test fun remote_newer_password_change_creates_browser_style_alert() {11        val local = VaultEntry(12            id = "login-1",13            title = "GitHub",14            password = "old",15            createdAt = 1L,16            modifiedAt = 10L,17        )18        val remote = local.copy(password = "new", modifiedAt = 20L)1920        val alerts = remoteEntrySyncAlerts(21            localEntries = listOf(local),22            remoteEntries = listOf(remote),23            deviceName = "MacBook",24            nowMs = 30L,25        )2627        assertEquals(1, alerts.size)28        assertEquals(SyncAlertType.REMOTE_PASSWORD_CHANGE, alerts.single().type)29        assertEquals("GitHub", alerts.single().entryTitle)30        assertEquals("MacBook changed the password for GitHub", syncAlertMessage(alerts.single()))31    }3233    @Test fun stale_or_same_password_entries_do_not_alert() {34        val local = VaultEntry(35            id = "login-1",36            title = "Mail",37            password = "same",38            createdAt = 1L,39            modifiedAt = 10L,40        )4142        val alerts = remoteEntrySyncAlerts(43            localEntries = listOf(local),44            remoteEntries = listOf(45                local.copy(password = "different", modifiedAt = 9L),46                local.copy(password = "same", modifiedAt = 20L),47            ),48            deviceName = "paired device",49            nowMs = 30L,50        )5152        assertEquals(emptyList<SyncAlert>(), alerts)53    }5455    @Test fun winning_remote_delete_creates_delete_alert() {56        val local = VaultEntry(57            id = "login-1",58            title = "Bank",59            password = "pw",60            createdAt = 1L,61            modifiedAt = 10L,62        )6364        val alerts = remoteTombstoneSyncAlerts(65            localEntries = listOf(local),66            remoteTombstones = listOf(Tombstone("login-1", deletedAt = 10L)),67            deviceName = "Browser",68            nowMs = 40L,69        )7071        assertEquals(1, alerts.size)72        assertEquals(SyncAlertType.REMOTE_DELETE, alerts.single().type)73        assertEquals("Browser deleted Bank", syncAlertMessage(alerts.single()))74    }7576    @Test fun device_key_rotation_detects_existing_peer_key_changes() {77        val existing = PairedDevice(78            deviceId = "browser",79            deviceName = "Browser",80            deviceType = "extension",81            longTermPublicKeyHex = "aa".repeat(32),82            signingPublicKeyHex = "bb".repeat(32),83            pairedAt = 1L,84        )85        val incoming = ManifestDeviceEntry(86            deviceId = "browser",87            deviceName = "Browser",88            deviceType = "extension",89            longTermPublicKeyHex = "aa".repeat(32),90            signingPublicKeyHex = "cc".repeat(32),91            pairedAt = 1L,92        )9394        val alert = deviceKeyRotationAlert(existing, incoming, nowMs = 99L)9596        assertEquals(SyncAlertType.DEVICE_KEY_ROTATED, alert?.type)97        assertEquals("Browser", alert?.deviceName)98        assertNull(deviceKeyRotationAlert(existing, incoming.copy(signingPublicKeyHex = "bb".repeat(32)), nowMs = 99L))99    }100101    @Test fun `S5 rotation alert names the device from local state not the manifest`() {102        // A hostile issuer relabels the rotating device, hoping the alert reads103        // as a rotation on something the user trusts more.104        val existing = PairedDevice(105            deviceId = "browser",106            deviceName = "Spare Tablet",107            deviceType = "extension",108            longTermPublicKeyHex = "aa".repeat(32),109            signingPublicKeyHex = "bb".repeat(32),110            pairedAt = 1L,111        )112        val incoming = ManifestDeviceEntry(113            deviceId = "browser",114            deviceName = "June's Primary Phone",115            deviceType = "extension",116            longTermPublicKeyHex = "aa".repeat(32),117            signingPublicKeyHex = "cc".repeat(32),118            pairedAt = 1L,119        )120121        val alert = deviceKeyRotationAlert(existing, incoming, nowMs = 99L)122123        assertEquals("Spare Tablet", alert?.deviceName)124    }125126    @Test fun sync_alert_queue_is_bounded_to_latest_two_hundred() {127        val existing = (0 until 199).map { SyncAlert(SyncAlertType.REMOTE_DELETE, timestamp = it.toLong()) }128        val incoming = listOf(129            SyncAlert(SyncAlertType.REMOTE_DELETE, timestamp = 199L),130            SyncAlert(SyncAlertType.REMOTE_DELETE, timestamp = 200L),131        )132133        val bounded = boundedSyncAlerts(existing, incoming)134135        assertEquals(200, bounded.size)136        assertEquals(1L, bounded.first().timestamp)137        assertEquals(200L, bounded.last().timestamp)138    }139}