AutofillMatchTest.kt

5.5 KB · raw

1package com.peachpasswords.autofill23import com.peachpasswords.vault.VaultEntry4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78class AutofillMatchTest {9    private fun entry(title: String, url: String?) =10        VaultEntry(id = title, title = title, url = url, createdAt = 0, modifiedAt = 0)1112    @Test fun strict_domain_match_hits_only_the_matching_site() {13        val peach = entry("Peach", "https://peachpasswords.com/login")14        val mail = entry("Fastmail", "https://fastmail.com")15        val result = AutofillResponseBuilder.matchEntries(listOf(mail, peach), "peachpasswords.com")16        assertEquals(listOf(peach), result.hits)17        assertTrue("no unrelated entries leak for a web origin", result.rest.isEmpty())18    }1920    @Test fun no_false_positive_on_substring_lookalike() {21        val gh = entry("GitHub", "https://github.com/login")22        val result = AutofillResponseBuilder.matchEntries(listOf(gh), "notgithub.com")23        assertTrue("github.com must not match notgithub.com", result.hits.isEmpty())24        assertTrue(result.rest.isEmpty())25    }2627    @Test fun subdomain_origin_matches_registrable_domain() {28        val peach = entry("Peach", "https://peachpasswords.com")29        val result = AutofillResponseBuilder.matchEntries(listOf(peach), "app.peachpasswords.com")30        assertEquals(listOf(peach), result.hits)31    }3233    @Test fun additional_urls_match_registrable_domain() {34        val peach = entry("Peach", "https://peachpasswords.com")35            .copy(additionalUrls = listOf("https://accounts.peachpasswords.co.uk/login"))36        val result = AutofillResponseBuilder.matchEntries(listOf(peach), "signin.peachpasswords.co.uk")37        assertEquals(listOf(peach), result.hits)38    }3940    @Test fun additional_urls_do_not_match_substring_lookalikes() {41        val gh = entry("GitHub", null)42            .copy(additionalUrls = listOf("https://github.com/login"))43        val result = AutofillResponseBuilder.matchEntries(listOf(gh), "evilgithub.com")44        assertTrue(result.hits.isEmpty())45        assertTrue(result.rest.isEmpty())46    }4748    @Test fun picker_web_origin_uses_domain_matching() {49        val peach = entry("Peach", "https://peachpasswords.com")50        val parsed = ParsedStructure(51            webDomain = "app.peachpasswords.com",52            originDomains = listOf("peachpasswords.com"),53        )54        assertEquals(listOf(peach), autofillPickerEntries(listOf(peach), parsed, limit = 12))55    }5657    @Test fun picker_native_origin_matches_app_link_domains_and_associated_package() {58        val byDomain = entry("Native", "https://example.com/login")59        val associated = entry("Saved here", "com.example.app")60        val other = entry("Other", "https://other.example.net")61        val parsed = ParsedStructure(62            packageName = "com.example.app",63            originDomains = listOf("example.com"),64        )65        assertEquals(66            listOf(byDomain, associated),67            autofillPickerEntries(listOf(byDomain, associated, other), parsed, limit = 12),68        )69    }7071    /** PAI-2026-001: a display label is presentation, not identity. */72    @Test fun native_app_label_never_selects_a_credential() {73        val bank = entry("Bank", "https://real-bank.test/login")74        val parsed = ParsedStructure(packageName = "com.attacker.app")75        assertTrue(76            "a sideloaded app named after a vault entry must not be offered it",77            autofillPickerEntries(listOf(bank), parsed, limit = 12).isEmpty(),78        )79    }8081    @Test fun associated_package_match_is_exact_not_registrable_domain() {82        val mine = entry("Mine", "com.a.example.app")83        val theirs = entry("Theirs", "com.b.example.app")84        val parsed = ParsedStructure(packageName = "com.a.example.app")85        assertEquals(86            listOf(mine),87            autofillPickerEntries(listOf(mine, theirs), parsed, limit = 12),88        )89    }9091    @Test fun origin_without_domains_or_title_has_no_matches() {92        val app = entry("Native", "https://example.com/login")93        val parsed = ParsedStructure(packageName = "com.example.app")94        assertTrue(autofillPickerEntries(listOf(app), parsed, limit = 12).isEmpty())95    }9697    @Test fun picker_label_routes_locked_users_to_unlock() {98        assertEquals("Unlock Peach", autofillPickerLabel(unlocked = false, datasetCount = 3))99    }100101    @Test fun picker_label_names_no_match_state() {102        assertEquals("No saved logins for this site", autofillPickerLabel(unlocked = true, datasetCount = 0))103    }104105    @Test fun picker_label_keeps_search_affordance_when_matches_exist() {106        assertEquals("Search Peach…", autofillPickerLabel(unlocked = true, datasetCount = 1))107    }108109    @Test fun null_origin_yields_no_entries() {110        val a = entry("A", "https://a.com")111        val b = entry("B", "https://b.com")112        val result = AutofillResponseBuilder.matchEntries(listOf(a, b), null)113        assertTrue(result.all.isEmpty())114    }115116    @Test fun web_origin_matches_its_page_domain() {117        val bank = entry("Bank", "https://bank.example.com")118        val parsed = ParsedStructure(119            webDomain = "bank.example.com",120            originDomains = listOf("bank.example.com"),121        )122        assertEquals(listOf(bank), AutofillResponseBuilder.matchEntries(listOf(bank), parsed).all)123    }124125    @Test fun parser_identity_uses_only_framework_activity_component() {126        assertEquals(127            "com.example.host",128            authenticatedPackageName("com.example.host"),129        )130        assertEquals(null, authenticatedPackageName(null))131    }132}133