package com.peachpasswords.vault import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Test /** * MN-3: sync-peer entries are sanitized before merge — mirrors the browser's * sanitizeRestoredEntry, with the Android allowance for flat passkey fields. */ class SyncEntrySanitizerTest { private val historyMax = 50 @Test fun blank_titles_are_dropped() { assertNull(SyncEntrySanitizer.sanitize(entry(title = ""), historyMax)) assertNull(SyncEntrySanitizer.sanitize(entry(title = " "), historyMax)) } @Test fun titles_are_trimmed_and_valid_entries_pass() { val sanitized = SyncEntrySanitizer.sanitize(entry(title = " GitHub "), historyMax) assertEquals("GitHub", sanitized?.title) } @Test fun credential_less_logins_are_dropped() { assertNull(SyncEntrySanitizer.sanitize(entry(password = null), historyMax)) assertNotNull(SyncEntrySanitizer.sanitize(entry(password = "hunter2"), historyMax)) } @Test fun passkey_only_logins_survive_nested_or_flat() { // Nested passkey, no password — the modern shape. val nested = entry(password = null).copy(passkey = passkey()) assertNotNull(SyncEntrySanitizer.sanitize(nested, historyMax)) // Flat passkey fields (browser legacy shape) — consolidated on unlock. val flat = entry(password = null).copy(rpId = "example.com", credentialId = "cred-1") assertNotNull(SyncEntrySanitizer.sanitize(flat, historyMax)) } @Test fun husk_passkeys_drop_the_whole_entry() { val husk = entry(password = null).copy(passkey = passkey(credentialId = "")) assertNull(SyncEntrySanitizer.sanitize(husk, historyMax)) val noKey = entry(password = null).copy(passkey = passkey(publicKeyCose = " ")) assertNull(SyncEntrySanitizer.sanitize(noKey, historyMax)) } @Test fun non_login_types_need_no_password() { val card = entry(type = "card", password = null).copy(cardNumber = "4111111111111111") assertNotNull(SyncEntrySanitizer.sanitize(card, historyMax)) } @Test fun password_history_is_re_capped_on_merge() { val bloated = (1..(historyMax + 25)).map { PasswordHistoryItem(password = "pw-$it", changedAt = it.toLong()) } val sanitized = SyncEntrySanitizer.sanitize(entry(passwordHistory = bloated), historyMax) assertEquals(historyMax, sanitized?.passwordHistory?.size) assertEquals("pw-1", sanitized?.passwordHistory?.first()?.password) val small = listOf(PasswordHistoryItem(password = "old", changedAt = 1)) assertEquals(small, SyncEntrySanitizer.sanitize(entry(passwordHistory = small), historyMax)?.passwordHistory) assertNull(SyncEntrySanitizer.sanitize(entry(passwordHistory = null), historyMax)?.passwordHistory) } private fun entry( title: String = "Example", type: String? = "login", password: String? = "secret", passwordHistory: List? = null, ) = VaultEntry( id = "entry-1", type = type, title = title, password = password, passwordHistory = passwordHistory, createdAt = 1, modifiedAt = 2, ) private fun passkey(credentialId: String = "cred-1", publicKeyCose: String = "cose") = PasskeyData( credentialId = credentialId, publicKeyCose = publicKeyCose, rpId = "example.com", userHandle = "handle", userName = "june", ) }