package com.peachpasswords.vault import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test /** * VaultHealth suite (brief Suite 8): the pure [VaultHealth.buildReport] scoring — * breach / reused / weak / aged / sensitivity detection, section counts, and the * sensitivity-weighted vault score. (HIBP k-anonymity lives in the network layer.) */ class VaultHealthTest { private val DAY = 24L * 60 * 60 * 1000 private val now = System.currentTimeMillis() private fun login( id: String, password: String? = "Str0ng-Pass!xyz", url: String? = null, breach: Int? = null, totp: String? = null, changedAt: Long? = null, ) = VaultEntry( id = id, type = "login", title = id, password = password, url = url, breach = breach, totp = totp, passwordChangedAt = changedAt, createdAt = 0, modifiedAt = 0, ) private fun scoreOf(report: VaultHealth.VaultHealthReport, id: String) = report.entries.first { it.entryId == id } // ── Breach ──────────────────────────────────────────────────────────────── @Test fun breached_entry_flagged_and_counted() { val r = VaultHealth.buildReport(listOf(login("a", breach = 3))) assertTrue(scoreOf(r, "a").isBreach) assertEquals(1, r.breachCount) } @Test fun unbreached_entry_not_flagged() { val r = VaultHealth.buildReport(listOf(login("a", breach = 0), login("b"))) assertFalse(scoreOf(r, "a").isBreach) assertEquals(0, r.breachCount) } // ── Reuse ───────────────────────────────────────────────────────────────── @Test fun reused_password_flagged_on_both_entries() { val r = VaultHealth.buildReport(listOf(login("a", password = "Shared-Pass!1"), login("b", password = "Shared-Pass!1"))) assertTrue(scoreOf(r, "a").isReused) assertTrue(scoreOf(r, "b").isReused) assertEquals(2, r.reusedCount) } @Test fun unique_passwords_not_reused() { val r = VaultHealth.buildReport(listOf(login("a", password = "Uniq-Pass!1"), login("b", password = "Other-Pass!2"))) assertFalse(scoreOf(r, "a").isReused) assertEquals(0, r.reusedCount) } // ── Weakness ────────────────────────────────────────────────────────────── @Test fun short_password_is_weak() { assertTrue(scoreOf(VaultHealth.buildReport(listOf(login("a", password = "Ab1!"))), "a").isWeak) } @Test fun low_char_class_password_is_weak() { // 19 lowercase letters: long enough, but only one character class (< 3). assertTrue(scoreOf(VaultHealth.buildReport(listOf(login("a", password = "abcdefghijklmnop"))), "a").isWeak) } @Test fun strong_password_not_weak() { assertFalse(scoreOf(VaultHealth.buildReport(listOf(login("a", password = "Str0ng-Pass!xyz"))), "a").isWeak) } // ── Age ─────────────────────────────────────────────────────────────────── @Test fun old_password_is_aged() { val r = VaultHealth.buildReport(listOf(login("a", changedAt = now - 200 * DAY))) assertTrue(scoreOf(r, "a").isAged) assertEquals(1, r.agedCount) } @Test fun recent_password_not_aged() { val r = VaultHealth.buildReport(listOf(login("a", changedAt = now - 5 * DAY))) assertFalse(scoreOf(r, "a").isAged) } @Test fun sensitive_entry_has_tighter_age_threshold() { // 100 days: aged for a sensitive (90d) entry, fresh for a standard (180d) one. val sensitive = login("s", url = "https://mybank.com", changedAt = now - 100 * DAY) val standard = login("n", url = "https://example.com", changedAt = now - 100 * DAY) val r = VaultHealth.buildReport(listOf(sensitive, standard)) assertTrue(scoreOf(r, "s").isAged) assertFalse(scoreOf(r, "n").isAged) } // ── Sensitivity / TOTP ──────────────────────────────────────────────────── @Test fun sensitive_domain_detected() { assertTrue(scoreOf(VaultHealth.buildReport(listOf(login("a", url = "https://gmail.com"))), "a").isSensitive) } @Test fun non_sensitive_domain_not_flagged() { assertFalse(scoreOf(VaultHealth.buildReport(listOf(login("a", url = "https://example.com"))), "a").isSensitive) } @Test fun sensitive_without_totp_flags_missing_totp() { val r = VaultHealth.buildReport(listOf(login("a", url = "https://gmail.com"))) assertTrue(scoreOf(r, "a").missingTotp) } @Test fun sensitive_with_totp_does_not_flag_missing_totp() { val r = VaultHealth.buildReport(listOf(login("a", url = "https://gmail.com", totp = "JBSWY3DPEHPK3PXP"))) assertFalse(scoreOf(r, "a").missingTotp) } // ── Aggregate / vault score ─────────────────────────────────────────────── @Test fun all_healthy_vault_scores_zero() { val r = VaultHealth.buildReport(listOf( login("a", password = "Str0ng-Pass!xyz", url = "https://example.com", changedAt = now), login("b", password = "An0ther-Pass!qq", url = "https://other.example.com", changedAt = now), )) assertEquals(0, r.vaultScore) assertEquals(0, r.breachCount + r.reusedCount + r.agedCount + r.weakCount) } @Test fun empty_vault_scores_zero() { assertEquals(0, VaultHealth.buildReport(emptyList()).vaultScore) } @Test fun breach_dominates_entry_score() { // Breach contributes 60; the per-entry score is capped at 100. val s = scoreOf(VaultHealth.buildReport(listOf(login("a", breach = 1))), "a") assertTrue(s.score >= 60) assertTrue(s.score <= 100) } @Test fun non_login_and_deleted_entries_excluded() { val card = VaultEntry(id = "c", type = "card", title = "Visa", cardNumber = "4242", createdAt = 0, modifiedAt = 0) val note = VaultEntry(id = "n", type = "note", title = "memo", createdAt = 0, modifiedAt = 0) val deleted = login("d", breach = 9).copy(deletedAt = now) val live = login("l") val r = VaultHealth.buildReport(listOf(card, note, deleted, live)) assertEquals(1, r.entries.size) assertEquals("l", r.entries.first().entryId) assertEquals(0, r.breachCount) // the breached entry was deleted → excluded } @Test fun sensitive_entries_weigh_double_in_vault_score() { // One perfectly healthy standard entry and one fully-compromised sensitive entry. val healthy = login("h", password = "Str0ng-Pass!xyz", url = "https://example.com", changedAt = now) val bad = login("b", password = "abc", url = "https://mybank.com", breach = 5, changedAt = now - 400 * DAY) val r = VaultHealth.buildReport(listOf(healthy, bad)) // bad entry score = 100, weight 2; healthy = 0, weight 1 → 200/3 ≈ 66. assertEquals(66, r.vaultScore) } }