package com.peachpasswords.credentials import com.peachpasswords.vault.VaultEntry import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test class PasswordCredentialPlannerTest { private val SIGNER = "AA:BB:CC:DD" private fun entry( title: String, url: String?, username: String? = "june@example.com", password: String? = "correct-horse-battery", lastUsedAt: Long? = null, ) = VaultEntry( id = title, title = title, url = url, username = username, password = password, createdAt = 0, modifiedAt = 0, lastUsedAt = lastUsedAt, ) @Test fun web_caller_matches_entry_on_same_registrable_domain() { val peach = entry("Peach", "https://peachpasswords.com/login") val mail = entry("Fastmail", "https://fastmail.com") val hits = PasswordCredentialPlanner.candidates(listOf(mail, peach), setOf("peachpasswords.com"), null) assertEquals(listOf("Peach"), hits.map { it.title }) } @Test fun subdomain_entry_matches_registrable_caller_domain() { val peach = entry("Peach", "https://app.peachpasswords.com") val hits = PasswordCredentialPlanner.candidates(listOf(peach), setOf("peachpasswords.com"), null) assertEquals(1, hits.size) } @Test fun no_false_positive_on_substring_lookalike() { val gh = entry("GitHub", "https://github.com/login") val hits = PasswordCredentialPlanner.candidates(listOf(gh), setOf("notgithub.com"), null) assertTrue("github.com must not match notgithub.com", hits.isEmpty()) } @Test fun additional_urls_match_registrable_domain() { val peach = entry("Peach", "https://peachpasswords.com") .copy(additionalUrls = listOf("https://accounts.peachpasswords.co.uk/login")) val hits = PasswordCredentialPlanner.candidates(listOf(peach), setOf("peachpasswords.co.uk"), null) assertEquals(1, hits.size) } /** PAI-2026-001: an app's display label is not a credential origin. */ @Test fun native_caller_does_not_match_by_app_title() { val netflix = entry("Netflix", null) val hits = PasswordCredentialPlanner.candidates(listOf(netflix), emptySet(), "Netflix") assertTrue("a mutable app label must never select a credential", hits.isEmpty()) } @Test fun native_caller_matches_an_entry_associated_with_its_package() { val saved = entry("Saved here", "com.example.app").copy(nativeSignerDigests = setOf(SIGNER)) val hits = PasswordCredentialPlanner.candidates( listOf(saved), emptySet(), "com.example.app", null, setOf(SIGNER), ) assertEquals(listOf("Saved here"), hits.map { it.title }) } @Test fun associated_package_match_is_exact_not_registrable_domain() { val mine = entry("Mine", "com.a.example.app").copy(nativeSignerDigests = setOf(SIGNER)) val theirs = entry("Theirs", "com.b.example.app").copy(nativeSignerDigests = setOf(SIGNER)) val hits = PasswordCredentialPlanner.candidates( listOf(mine, theirs), emptySet(), "com.a.example.app", null, setOf(SIGNER), ) assertEquals(listOf("Mine"), hits.map { it.title }) } /** PB-26-134: a package name alone can be reused by a re-signed app. */ @Test fun native_caller_gets_no_entry_whose_signer_was_never_recorded() { val legacy = entry("Legacy", "com.example.app") val hits = PasswordCredentialPlanner.candidates( listOf(legacy), emptySet(), "com.example.app", null, setOf(SIGNER), ) assertTrue("an entry with no recorded signer must fail closed", hits.isEmpty()) } @Test fun entries_without_password_are_never_offered() { val notes = entry("Notes", "https://example.com", password = null) val blank = entry("Blank", "https://example.com", password = " ") val hits = PasswordCredentialPlanner.candidates(listOf(notes, blank), setOf("example.com"), null) assertTrue(hits.isEmpty()) } @Test fun deleted_entries_are_never_offered() { val gone = entry("Gone", "https://example.com").copy(deletedAt = 1L) val hits = PasswordCredentialPlanner.candidates(listOf(gone), setOf("example.com"), null) assertTrue(hits.isEmpty()) } @Test fun entries_without_any_username_are_skipped() { // Credential Manager requires an id; an entry with neither username nor // email cannot produce a PasswordCredential. val nameless = entry("Nameless", "https://example.com", username = null) val hits = PasswordCredentialPlanner.candidates(listOf(nameless), setOf("example.com"), null) assertTrue(hits.isEmpty()) } @Test fun email_is_used_when_username_is_missing() { val emailOnly = entry("Mail", "https://example.com", username = null) .copy(email = "june@example.com") val hits = PasswordCredentialPlanner.candidates(listOf(emailOnly), setOf("example.com"), null) assertEquals(listOf("june@example.com"), hits.map { it.username }) } @Test fun allowed_user_ids_filter_restricts_rows() { val a = entry("A", "https://example.com", username = "alice") val b = entry("B", "https://example.com", username = "bob") val hits = PasswordCredentialPlanner.candidates( listOf(a, b), setOf("example.com"), null, allowedUserIds = setOf("bob"), ) assertEquals(listOf("bob"), hits.map { it.username }) } @Test fun most_recently_used_ranks_first() { val old = entry("Old", "https://example.com", username = "old", lastUsedAt = 100L) val fresh = entry("Fresh", "https://example.com", username = "fresh", lastUsedAt = 200L) val hits = PasswordCredentialPlanner.candidates(listOf(old, fresh), setOf("example.com"), null) assertEquals(listOf("fresh", "old"), hits.map { it.username }) } @Test fun duplicate_entries_yield_one_row() { val peach = entry("Peach", "https://peachpasswords.com") val hits = PasswordCredentialPlanner.candidates( listOf(peach, peach), setOf("peachpasswords.com"), ) assertEquals(1, hits.size) } }