AutofillMatchTest.kt

6.3 KB · raw

1package com.peachpasswords.autofill23import com.peachpasswords.vault.VaultEntry4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78class AutofillMatchTest {9    private val SIGNER = "AA:BB:CC:DD"1011    private fun entry(title: String, url: String?) =12        VaultEntry(id = title, title = title, url = url, createdAt = 0, modifiedAt = 0)1314    @Test fun strict_domain_match_hits_only_the_matching_site() {15        val peach = entry("Peach", "https://peachpasswords.com/login")16        val mail = entry("Fastmail", "https://fastmail.com")17        val result = AutofillResponseBuilder.matchEntries(listOf(mail, peach), "peachpasswords.com")18        assertEquals(listOf(peach), result.hits)19        assertTrue("no unrelated entries leak for a web origin", result.rest.isEmpty())20    }2122    @Test fun no_false_positive_on_substring_lookalike() {23        val gh = entry("GitHub", "https://github.com/login")24        val result = AutofillResponseBuilder.matchEntries(listOf(gh), "notgithub.com")25        assertTrue("github.com must not match notgithub.com", result.hits.isEmpty())26        assertTrue(result.rest.isEmpty())27    }2829    @Test fun subdomain_origin_matches_registrable_domain() {30        val peach = entry("Peach", "https://peachpasswords.com")31        val result = AutofillResponseBuilder.matchEntries(listOf(peach), "app.peachpasswords.com")32        assertEquals(listOf(peach), result.hits)33    }3435    @Test fun additional_urls_match_registrable_domain() {36        val peach = entry("Peach", "https://peachpasswords.com")37            .copy(additionalUrls = listOf("https://accounts.peachpasswords.co.uk/login"))38        val result = AutofillResponseBuilder.matchEntries(listOf(peach), "signin.peachpasswords.co.uk")39        assertEquals(listOf(peach), result.hits)40    }4142    @Test fun additional_urls_do_not_match_substring_lookalikes() {43        val gh = entry("GitHub", null)44            .copy(additionalUrls = listOf("https://github.com/login"))45        val result = AutofillResponseBuilder.matchEntries(listOf(gh), "evilgithub.com")46        assertTrue(result.hits.isEmpty())47        assertTrue(result.rest.isEmpty())48    }4950    @Test fun picker_web_origin_uses_domain_matching() {51        val peach = entry("Peach", "https://peachpasswords.com")52        val parsed = ParsedStructure(53            webDomain = "app.peachpasswords.com",54            originDomains = listOf("peachpasswords.com"),55        )56        assertEquals(listOf(peach), autofillPickerEntries(listOf(peach), parsed, limit = 12))57    }5859    @Test fun picker_native_origin_matches_app_link_domains_and_associated_package() {60        val byDomain = entry("Native", "https://example.com/login")61        val associated = entry("Saved here", "com.example.app").copy(nativeSignerDigests = setOf(SIGNER))62        val other = entry("Other", "https://other.example.net")63        val parsed = ParsedStructure(64            packageName = "com.example.app",65            originDomains = listOf("example.com"),66            nativeSignerDigests = setOf(SIGNER),67        )68        assertEquals(69            listOf(byDomain, associated),70            autofillPickerEntries(listOf(byDomain, associated, other), parsed, limit = 12),71        )72    }7374    /** PAI-2026-001: a display label is presentation, not identity. */75    @Test fun native_app_label_never_selects_a_credential() {76        val bank = entry("Bank", "https://real-bank.test/login")77        val parsed = ParsedStructure(packageName = "com.attacker.app")78        assertTrue(79            "a sideloaded app named after a vault entry must not be offered it",80            autofillPickerEntries(listOf(bank), parsed, limit = 12).isEmpty(),81        )82    }8384    @Test fun associated_package_match_is_exact_not_registrable_domain() {85        val mine = entry("Mine", "com.a.example.app").copy(nativeSignerDigests = setOf(SIGNER))86        val theirs = entry("Theirs", "com.b.example.app").copy(nativeSignerDigests = setOf(SIGNER))87        val parsed = ParsedStructure(packageName = "com.a.example.app", nativeSignerDigests = setOf(SIGNER))88        assertEquals(89            listOf(mine),90            autofillPickerEntries(listOf(mine, theirs), parsed, limit = 12),91        )92    }9394    /** PB-26-134: a package name alone can be reused after uninstall. */95    @Test fun native_origin_fails_closed_when_no_signer_was_recorded() {96        val legacy = entry("Legacy", "com.example.app")97        val parsed = ParsedStructure(packageName = "com.example.app", nativeSignerDigests = setOf(SIGNER))98        assertTrue(99            "an entry with no recorded signer must not be offered to a native caller",100            autofillPickerEntries(listOf(legacy), parsed, limit = 12).isEmpty(),101        )102    }103104    @Test fun origin_without_domains_or_title_has_no_matches() {105        val app = entry("Native", "https://example.com/login")106        val parsed = ParsedStructure(packageName = "com.example.app")107        assertTrue(autofillPickerEntries(listOf(app), parsed, limit = 12).isEmpty())108    }109110    @Test fun picker_label_routes_locked_users_to_unlock() {111        assertEquals("Unlock Peach", autofillPickerLabel(unlocked = false, datasetCount = 3))112    }113114    @Test fun picker_label_names_no_match_state() {115        assertEquals("No saved logins for this site", autofillPickerLabel(unlocked = true, datasetCount = 0))116    }117118    @Test fun picker_label_keeps_search_affordance_when_matches_exist() {119        assertEquals("Search Peach…", autofillPickerLabel(unlocked = true, datasetCount = 1))120    }121122    @Test fun null_origin_yields_no_entries() {123        val a = entry("A", "https://a.com")124        val b = entry("B", "https://b.com")125        val result = AutofillResponseBuilder.matchEntries(listOf(a, b), null)126        assertTrue(result.all.isEmpty())127    }128129    @Test fun web_origin_matches_its_page_domain() {130        val bank = entry("Bank", "https://bank.example.com")131        val parsed = ParsedStructure(132            webDomain = "bank.example.com",133            originDomains = listOf("bank.example.com"),134        )135        assertEquals(listOf(bank), AutofillResponseBuilder.matchEntries(listOf(bank), parsed).all)136    }137138    @Test fun parser_identity_uses_only_framework_activity_component() {139        assertEquals(140            "com.example.host",141            authenticatedPackageName("com.example.host"),142        )143        assertEquals(null, authenticatedPackageName(null))144    }145}146