package com.peachpasswords.autofill import com.peachpasswords.vault.VaultEntry import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test class AutofillMatchTest { private fun entry(title: String, url: String?) = VaultEntry(id = title, title = title, url = url, createdAt = 0, modifiedAt = 0) @Test fun strict_domain_match_hits_only_the_matching_site() { val peach = entry("Peach", "https://peachpasswords.com/login") val mail = entry("Fastmail", "https://fastmail.com") val result = AutofillResponseBuilder.matchEntries(listOf(mail, peach), "peachpasswords.com") assertEquals(listOf(peach), result.hits) assertTrue("no unrelated entries leak for a web origin", result.rest.isEmpty()) } @Test fun no_false_positive_on_substring_lookalike() { val gh = entry("GitHub", "https://github.com/login") val result = AutofillResponseBuilder.matchEntries(listOf(gh), "notgithub.com") assertTrue("github.com must not match notgithub.com", result.hits.isEmpty()) assertTrue(result.rest.isEmpty()) } @Test fun subdomain_origin_matches_registrable_domain() { val peach = entry("Peach", "https://peachpasswords.com") val result = AutofillResponseBuilder.matchEntries(listOf(peach), "app.peachpasswords.com") assertEquals(listOf(peach), result.hits) } @Test fun additional_urls_match_registrable_domain() { val peach = entry("Peach", "https://peachpasswords.com") .copy(additionalUrls = listOf("https://accounts.peachpasswords.co.uk/login")) val result = AutofillResponseBuilder.matchEntries(listOf(peach), "signin.peachpasswords.co.uk") assertEquals(listOf(peach), result.hits) } @Test fun additional_urls_do_not_match_substring_lookalikes() { val gh = entry("GitHub", null) .copy(additionalUrls = listOf("https://github.com/login")) val result = AutofillResponseBuilder.matchEntries(listOf(gh), "evilgithub.com") assertTrue(result.hits.isEmpty()) assertTrue(result.rest.isEmpty()) } @Test fun picker_web_origin_uses_domain_matching() { val peach = entry("Peach", "https://peachpasswords.com") val parsed = ParsedStructure( webDomain = "app.peachpasswords.com", originTrust = AutofillOriginTrust.TRUSTED_WEB, ) assertEquals(listOf(peach), autofillPickerEntries(listOf(peach), parsed, limit = 12)) } @Test fun picker_native_origin_uses_verified_app_link_domains() { val app = entry("Native", "https://example.com/login") val other = entry("Other", "https://other.example.net") val parsed = ParsedStructure( packageName = "com.example.app", originTrust = AutofillOriginTrust.TRUSTED_NATIVE, verifiedDomains = listOf("example.com"), ) assertEquals(listOf(app), autofillPickerEntries(listOf(app, other), parsed, limit = 12)) } @Test fun unknown_native_origin_has_no_matches() { val app = entry("Native", "https://example.com/login") val parsed = ParsedStructure(packageName = "com.example.app") assertTrue(autofillPickerEntries(listOf(app), parsed, limit = 12).isEmpty()) } @Test fun picker_label_routes_locked_users_to_unlock() { assertEquals("Unlock Peach", autofillPickerLabel(unlocked = false, datasetCount = 3)) } @Test fun picker_label_names_no_match_state() { assertEquals("No saved logins for this site", autofillPickerLabel(unlocked = true, datasetCount = 0)) } @Test fun picker_label_keeps_search_affordance_when_matches_exist() { assertEquals("Search Peach…", autofillPickerLabel(unlocked = true, datasetCount = 1)) } @Test fun null_origin_yields_no_entries() { val a = entry("A", "https://a.com") val b = entry("B", "https://b.com") val result = AutofillResponseBuilder.matchEntries(listOf(a, b), null) assertTrue(result.all.isEmpty()) } @Test fun unknown_origin_returns_no_preselected_credentials() { val bank = entry("Bank", "https://bank.example.com") val parsed = ParsedStructure( webDomain = "bank.example.com", packageName = "com.evil.phisher", originTrust = AutofillOriginTrust.UNKNOWN, ) assertTrue(AutofillResponseBuilder.matchEntries(listOf(bank), parsed).all.isEmpty()) } @Test fun trusted_browser_certificate_pins() { assertTrue( TrustedBrowsers.hasExpectedCertificate( "com.android.chrome", "f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83", ), ) assertTrue( TrustedBrowsers.hasExpectedCertificate( "org.mozilla.firefox", "a78b62a5165b4494b2fead9e76a280d22d937fee6251aece599446b2ea319b04", ), ) assertFalse(TrustedBrowsers.hasExpectedCertificate("com.android.chrome", "00".repeat(32))) assertFalse(TrustedBrowsers.hasExpectedCertificate("com.evil.phisher", "00".repeat(32))) assertFalse(TrustedBrowsers.hasExpectedCertificate(null, "00".repeat(32))) } @Test fun browser_pin_provenance_is_fail_closed_until_confirmed() { assertFalse(TrustedBrowsers.hasLocallyConfirmedPins("com.android.chrome")) assertFalse(TrustedBrowsers.hasLocallyConfirmedPins("com.evil.phisher")) } @Test fun api27_accepts_only_current_signers() { val expected = setOf("aa") assertTrue( TrustedBrowsers.certificateSetMatches( expected = expected, current = setOf("aa"), history = emptySet(), apiLevel = 27, ), ) assertFalse( TrustedBrowsers.certificateSetMatches( expected = expected, current = setOf("bb"), history = setOf("aa"), apiLevel = 27, ), ) } @Test fun api28_rejects_history_only_accepts_current_signers() { val expected = setOf("aa") // History-only match must NOT accept: the current signer is unpinned, // so the package is rejected even though rotation history contains a // pinned cert. History alone cannot prove the current binary is authentic. assertFalse( TrustedBrowsers.certificateSetMatches( expected = expected, current = setOf("bb"), history = setOf("aa"), apiLevel = 28, ), ) // Current match accepts. assertTrue( TrustedBrowsers.certificateSetMatches( expected = expected, current = setOf("aa"), history = emptySet(), apiLevel = 28, ), ) // No match rejects. assertFalse( TrustedBrowsers.certificateSetMatches( expected = expected, current = setOf("bb"), history = setOf("cc"), apiLevel = 28, ), ) } @Test fun parser_identity_uses_only_framework_activity_component() { assertEquals( "com.example.host", authenticatedPackageName("com.example.host"), ) assertEquals(null, authenticatedPackageName(null)) } }