VaultHealthTest.kt

7.5 KB · raw

1package com.peachpasswords.vault23import org.junit.Assert.assertEquals4import org.junit.Assert.assertFalse5import org.junit.Assert.assertTrue6import org.junit.Test78/**9 * VaultHealth suite (brief Suite 8): the pure [VaultHealth.buildReport] scoring —10 * breach / reused / weak / aged / sensitivity detection, section counts, and the11 * sensitivity-weighted vault score. (HIBP k-anonymity lives in the network layer.)12 */13class VaultHealthTest {1415    private val DAY = 24L * 60 * 60 * 100016    private val now = System.currentTimeMillis()1718    private fun login(19        id: String,20        password: String? = "Str0ng-Pass!xyz",21        url: String? = null,22        breach: Int? = null,23        totp: String? = null,24        changedAt: Long? = null,25    ) = VaultEntry(26        id = id, type = "login", title = id, password = password, url = url,27        breach = breach, totp = totp, passwordChangedAt = changedAt,28        createdAt = 0, modifiedAt = 0,29    )3031    private fun scoreOf(report: VaultHealth.VaultHealthReport, id: String) =32        report.entries.first { it.entryId == id }3334    // ── Breach ────────────────────────────────────────────────────────────────3536    @Test fun breached_entry_flagged_and_counted() {37        val r = VaultHealth.buildReport(listOf(login("a", breach = 3)))38        assertTrue(scoreOf(r, "a").isBreach)39        assertEquals(1, r.breachCount)40    }4142    @Test fun unbreached_entry_not_flagged() {43        val r = VaultHealth.buildReport(listOf(login("a", breach = 0), login("b")))44        assertFalse(scoreOf(r, "a").isBreach)45        assertEquals(0, r.breachCount)46    }4748    // ── Reuse ─────────────────────────────────────────────────────────────────4950    @Test fun reused_password_flagged_on_both_entries() {51        val r = VaultHealth.buildReport(listOf(login("a", password = "Shared-Pass!1"), login("b", password = "Shared-Pass!1")))52        assertTrue(scoreOf(r, "a").isReused)53        assertTrue(scoreOf(r, "b").isReused)54        assertEquals(2, r.reusedCount)55    }5657    @Test fun unique_passwords_not_reused() {58        val r = VaultHealth.buildReport(listOf(login("a", password = "Uniq-Pass!1"), login("b", password = "Other-Pass!2")))59        assertFalse(scoreOf(r, "a").isReused)60        assertEquals(0, r.reusedCount)61    }6263    // ── Weakness ──────────────────────────────────────────────────────────────6465    @Test fun short_password_is_weak() {66        assertTrue(scoreOf(VaultHealth.buildReport(listOf(login("a", password = "Ab1!"))), "a").isWeak)67    }6869    @Test fun low_char_class_password_is_weak() {70        // 19 lowercase letters: long enough, but only one character class (< 3).71        assertTrue(scoreOf(VaultHealth.buildReport(listOf(login("a", password = "abcdefghijklmnop"))), "a").isWeak)72    }7374    @Test fun strong_password_not_weak() {75        assertFalse(scoreOf(VaultHealth.buildReport(listOf(login("a", password = "Str0ng-Pass!xyz"))), "a").isWeak)76    }7778    // ── Age ───────────────────────────────────────────────────────────────────7980    @Test fun old_password_is_aged() {81        val r = VaultHealth.buildReport(listOf(login("a", changedAt = now - 200 * DAY)))82        assertTrue(scoreOf(r, "a").isAged)83        assertEquals(1, r.agedCount)84    }8586    @Test fun recent_password_not_aged() {87        val r = VaultHealth.buildReport(listOf(login("a", changedAt = now - 5 * DAY)))88        assertFalse(scoreOf(r, "a").isAged)89    }9091    @Test fun sensitive_entry_has_tighter_age_threshold() {92        // 100 days: aged for a sensitive (90d) entry, fresh for a standard (180d) one.93        val sensitive = login("s", url = "https://mybank.com", changedAt = now - 100 * DAY)94        val standard = login("n", url = "https://example.com", changedAt = now - 100 * DAY)95        val r = VaultHealth.buildReport(listOf(sensitive, standard))96        assertTrue(scoreOf(r, "s").isAged)97        assertFalse(scoreOf(r, "n").isAged)98    }99100    // ── Sensitivity / TOTP ────────────────────────────────────────────────────101102    @Test fun sensitive_domain_detected() {103        assertTrue(scoreOf(VaultHealth.buildReport(listOf(login("a", url = "https://gmail.com"))), "a").isSensitive)104    }105106    @Test fun non_sensitive_domain_not_flagged() {107        assertFalse(scoreOf(VaultHealth.buildReport(listOf(login("a", url = "https://example.com"))), "a").isSensitive)108    }109110    @Test fun sensitive_without_totp_flags_missing_totp() {111        val r = VaultHealth.buildReport(listOf(login("a", url = "https://gmail.com")))112        assertTrue(scoreOf(r, "a").missingTotp)113    }114115    @Test fun sensitive_with_totp_does_not_flag_missing_totp() {116        val r = VaultHealth.buildReport(listOf(login("a", url = "https://gmail.com", totp = "JBSWY3DPEHPK3PXP")))117        assertFalse(scoreOf(r, "a").missingTotp)118    }119120    // ── Aggregate / vault score ───────────────────────────────────────────────121122    @Test fun all_healthy_vault_scores_zero() {123        val r = VaultHealth.buildReport(listOf(124            login("a", password = "Str0ng-Pass!xyz", url = "https://example.com", changedAt = now),125            login("b", password = "An0ther-Pass!qq", url = "https://other.example.com", changedAt = now),126        ))127        assertEquals(0, r.vaultScore)128        assertEquals(0, r.breachCount + r.reusedCount + r.agedCount + r.weakCount)129    }130131    @Test fun empty_vault_scores_zero() {132        assertEquals(0, VaultHealth.buildReport(emptyList()).vaultScore)133    }134135    @Test fun breach_dominates_entry_score() {136        // Breach contributes 60; the per-entry score is capped at 100.137        val s = scoreOf(VaultHealth.buildReport(listOf(login("a", breach = 1))), "a")138        assertTrue(s.score >= 60)139        assertTrue(s.score <= 100)140    }141142    @Test fun non_login_and_deleted_entries_excluded() {143        val card = VaultEntry(id = "c", type = "card", title = "Visa", cardNumber = "4242", createdAt = 0, modifiedAt = 0)144        val note = VaultEntry(id = "n", type = "note", title = "memo", createdAt = 0, modifiedAt = 0)145        val deleted = login("d", breach = 9).copy(deletedAt = now)146        val live = login("l")147        val r = VaultHealth.buildReport(listOf(card, note, deleted, live))148        assertEquals(1, r.entries.size)149        assertEquals("l", r.entries.first().entryId)150        assertEquals(0, r.breachCount) // the breached entry was deleted → excluded151    }152153    @Test fun sensitive_entries_weigh_double_in_vault_score() {154        // One perfectly healthy standard entry and one fully-compromised sensitive entry.155        val healthy = login("h", password = "Str0ng-Pass!xyz", url = "https://example.com", changedAt = now)156        val bad = login("b", password = "abc", url = "https://mybank.com", breach = 5, changedAt = now - 400 * DAY)157        val r = VaultHealth.buildReport(listOf(healthy, bad))158        // bad entry score = 100, weight 2; healthy = 0, weight 1 → 200/3 ≈ 66.159        assertEquals(66, r.vaultScore)160    }161}