PasswordCredentialPlannerTest.kt

6.1 KB · raw

1package com.peachpasswords.credentials23import com.peachpasswords.vault.VaultEntry4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78class PasswordCredentialPlannerTest {9    private val SIGNER = "AA:BB:CC:DD"1011    private fun entry(12        title: String,13        url: String?,14        username: String? = "[email protected]",15        password: String? = "correct-horse-battery",16        lastUsedAt: Long? = null,17    ) = VaultEntry(18        id = title,19        title = title,20        url = url,21        username = username,22        password = password,23        createdAt = 0,24        modifiedAt = 0,25        lastUsedAt = lastUsedAt,26    )2728    @Test fun web_caller_matches_entry_on_same_registrable_domain() {29        val peach = entry("Peach", "https://peachpasswords.com/login")30        val mail = entry("Fastmail", "https://fastmail.com")31        val hits = PasswordCredentialPlanner.candidates(listOf(mail, peach), setOf("peachpasswords.com"), null)32        assertEquals(listOf("Peach"), hits.map { it.title })33    }3435    @Test fun subdomain_entry_matches_registrable_caller_domain() {36        val peach = entry("Peach", "https://app.peachpasswords.com")37        val hits = PasswordCredentialPlanner.candidates(listOf(peach), setOf("peachpasswords.com"), null)38        assertEquals(1, hits.size)39    }4041    @Test fun no_false_positive_on_substring_lookalike() {42        val gh = entry("GitHub", "https://github.com/login")43        val hits = PasswordCredentialPlanner.candidates(listOf(gh), setOf("notgithub.com"), null)44        assertTrue("github.com must not match notgithub.com", hits.isEmpty())45    }4647    @Test fun additional_urls_match_registrable_domain() {48        val peach = entry("Peach", "https://peachpasswords.com")49            .copy(additionalUrls = listOf("https://accounts.peachpasswords.co.uk/login"))50        val hits = PasswordCredentialPlanner.candidates(listOf(peach), setOf("peachpasswords.co.uk"), null)51        assertEquals(1, hits.size)52    }5354    /** PAI-2026-001: an app's display label is not a credential origin. */55    @Test fun native_caller_does_not_match_by_app_title() {56        val netflix = entry("Netflix", null)57        val hits = PasswordCredentialPlanner.candidates(listOf(netflix), emptySet(), "Netflix")58        assertTrue("a mutable app label must never select a credential", hits.isEmpty())59    }6061    @Test fun native_caller_matches_an_entry_associated_with_its_package() {62        val saved = entry("Saved here", "com.example.app").copy(nativeSignerDigests = setOf(SIGNER))63        val hits = PasswordCredentialPlanner.candidates(64            listOf(saved), emptySet(), "com.example.app", null, setOf(SIGNER),65        )66        assertEquals(listOf("Saved here"), hits.map { it.title })67    }6869    @Test fun associated_package_match_is_exact_not_registrable_domain() {70        val mine = entry("Mine", "com.a.example.app").copy(nativeSignerDigests = setOf(SIGNER))71        val theirs = entry("Theirs", "com.b.example.app").copy(nativeSignerDigests = setOf(SIGNER))72        val hits = PasswordCredentialPlanner.candidates(73            listOf(mine, theirs), emptySet(), "com.a.example.app", null, setOf(SIGNER),74        )75        assertEquals(listOf("Mine"), hits.map { it.title })76    }7778    /** PB-26-134: a package name alone can be reused by a re-signed app. */79    @Test fun native_caller_gets_no_entry_whose_signer_was_never_recorded() {80        val legacy = entry("Legacy", "com.example.app")81        val hits = PasswordCredentialPlanner.candidates(82            listOf(legacy), emptySet(), "com.example.app", null, setOf(SIGNER),83        )84        assertTrue("an entry with no recorded signer must fail closed", hits.isEmpty())85    }8687    @Test fun entries_without_password_are_never_offered() {88        val notes = entry("Notes", "https://example.com", password = null)89        val blank = entry("Blank", "https://example.com", password = "  ")90        val hits = PasswordCredentialPlanner.candidates(listOf(notes, blank), setOf("example.com"), null)91        assertTrue(hits.isEmpty())92    }9394    @Test fun deleted_entries_are_never_offered() {95        val gone = entry("Gone", "https://example.com").copy(deletedAt = 1L)96        val hits = PasswordCredentialPlanner.candidates(listOf(gone), setOf("example.com"), null)97        assertTrue(hits.isEmpty())98    }99100    @Test fun entries_without_any_username_are_skipped() {101        // Credential Manager requires an id; an entry with neither username nor102        // email cannot produce a PasswordCredential.103        val nameless = entry("Nameless", "https://example.com", username = null)104        val hits = PasswordCredentialPlanner.candidates(listOf(nameless), setOf("example.com"), null)105        assertTrue(hits.isEmpty())106    }107108    @Test fun email_is_used_when_username_is_missing() {109        val emailOnly = entry("Mail", "https://example.com", username = null)110            .copy(email = "[email protected]")111        val hits = PasswordCredentialPlanner.candidates(listOf(emailOnly), setOf("example.com"), null)112        assertEquals(listOf("[email protected]"), hits.map { it.username })113    }114115    @Test fun allowed_user_ids_filter_restricts_rows() {116        val a = entry("A", "https://example.com", username = "alice")117        val b = entry("B", "https://example.com", username = "bob")118        val hits = PasswordCredentialPlanner.candidates(119            listOf(a, b), setOf("example.com"), null, allowedUserIds = setOf("bob"),120        )121        assertEquals(listOf("bob"), hits.map { it.username })122    }123124    @Test fun most_recently_used_ranks_first() {125        val old = entry("Old", "https://example.com", username = "old", lastUsedAt = 100L)126        val fresh = entry("Fresh", "https://example.com", username = "fresh", lastUsedAt = 200L)127        val hits = PasswordCredentialPlanner.candidates(listOf(old, fresh), setOf("example.com"), null)128        assertEquals(listOf("fresh", "old"), hits.map { it.username })129    }130131    @Test fun duplicate_entries_yield_one_row() {132        val peach = entry("Peach", "https://peachpasswords.com")133        val hits = PasswordCredentialPlanner.candidates(134            listOf(peach, peach), setOf("peachpasswords.com"),135        )136        assertEquals(1, hits.size)137    }138}