SyncEntrySanitizerTest.kt

3.5 KB · raw

1package com.peachpasswords.vault23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNotNull5import org.junit.Assert.assertNull6import org.junit.Test78/**9 * MN-3: sync-peer entries are sanitized before merge — mirrors the browser's10 * sanitizeRestoredEntry, with the Android allowance for flat passkey fields.11 */12class SyncEntrySanitizerTest {13    private val historyMax = 501415    @Test fun blank_titles_are_dropped() {16        assertNull(SyncEntrySanitizer.sanitize(entry(title = ""), historyMax))17        assertNull(SyncEntrySanitizer.sanitize(entry(title = "   "), historyMax))18    }1920    @Test fun titles_are_trimmed_and_valid_entries_pass() {21        val sanitized = SyncEntrySanitizer.sanitize(entry(title = "  GitHub  "), historyMax)22        assertEquals("GitHub", sanitized?.title)23    }2425    @Test fun credential_less_logins_are_dropped() {26        assertNull(SyncEntrySanitizer.sanitize(entry(password = null), historyMax))27        assertNotNull(SyncEntrySanitizer.sanitize(entry(password = "hunter2"), historyMax))28    }2930    @Test fun passkey_only_logins_survive_nested_or_flat() {31        // Nested passkey, no password — the modern shape.32        val nested = entry(password = null).copy(passkey = passkey())33        assertNotNull(SyncEntrySanitizer.sanitize(nested, historyMax))34        // Flat passkey fields (browser legacy shape) — consolidated on unlock.35        val flat = entry(password = null).copy(rpId = "example.com", credentialId = "cred-1")36        assertNotNull(SyncEntrySanitizer.sanitize(flat, historyMax))37    }3839    @Test fun husk_passkeys_drop_the_whole_entry() {40        val husk = entry(password = null).copy(passkey = passkey(credentialId = ""))41        assertNull(SyncEntrySanitizer.sanitize(husk, historyMax))42        val noKey = entry(password = null).copy(passkey = passkey(publicKeyCose = " "))43        assertNull(SyncEntrySanitizer.sanitize(noKey, historyMax))44    }4546    @Test fun non_login_types_need_no_password() {47        val card = entry(type = "card", password = null).copy(cardNumber = "4111111111111111")48        assertNotNull(SyncEntrySanitizer.sanitize(card, historyMax))49    }5051    @Test fun password_history_is_re_capped_on_merge() {52        val bloated = (1..(historyMax + 25)).map { PasswordHistoryItem(password = "pw-$it", changedAt = it.toLong()) }53        val sanitized = SyncEntrySanitizer.sanitize(entry(passwordHistory = bloated), historyMax)54        assertEquals(historyMax, sanitized?.passwordHistory?.size)55        assertEquals("pw-1", sanitized?.passwordHistory?.first()?.password)5657        val small = listOf(PasswordHistoryItem(password = "old", changedAt = 1))58        assertEquals(small, SyncEntrySanitizer.sanitize(entry(passwordHistory = small), historyMax)?.passwordHistory)59        assertNull(SyncEntrySanitizer.sanitize(entry(passwordHistory = null), historyMax)?.passwordHistory)60    }6162    private fun entry(63        title: String = "Example",64        type: String? = "login",65        password: String? = "secret",66        passwordHistory: List<PasswordHistoryItem>? = null,67    ) = VaultEntry(68        id = "entry-1",69        type = type,70        title = title,71        password = password,72        passwordHistory = passwordHistory,73        createdAt = 1,74        modifiedAt = 2,75    )7677    private fun passkey(credentialId: String = "cred-1", publicKeyCose: String = "cose") = PasskeyData(78        credentialId = credentialId,79        publicKeyCose = publicKeyCose,80        rpId = "example.com",81        userHandle = "handle",82        userName = "june",83    )84}