PasswordStrengthTest.kt

3.0 KB · raw

1package com.peachpasswords.ui.security23import org.junit.Assert.assertEquals4import org.junit.Assert.assertTrue5import org.junit.Test67/**8 * Behavioural contract for the shared strength scorer. These assertions pin the9 * two gates the app relies on: CreateVault blocks `< 2`, ExportScreen blocks10 * `< 3`. They are intentionally inequality-based where the exact bucket is not11 * load-bearing, and exact where a boundary matters.12 */13class PasswordStrengthTest {1415    @Test fun empty_scores_zero() {16        assertEquals(0, scorePassword(""))17    }1819    @Test fun single_repeated_char_scores_zero() {20        assertEquals(0, scorePassword("aaaaaaaaaaaa"))21    }2223    @Test fun common_passwords_score_zero_case_insensitive() {24        assertEquals(0, scorePassword("password"))25        assertEquals(0, scorePassword("Password"))26        assertEquals(0, scorePassword("PASSWORD"))27        assertEquals(0, scorePassword("123456"))28        assertEquals(0, scorePassword("qwerty"))29        assertEquals(0, scorePassword("peach"))30    }3132    @Test fun short_single_class_is_weak() {33        // 8 lowercase letters: real but low-entropy → "weak" (1), below both gates.34        assertTrue(scorePassword("abcdefgh") < 2)35    }3637    @Test fun straight_sequence_is_penalized() {38        // Pure ascending run collapses toward zero entropy.39        assertTrue(scorePassword("abcdefghij") < 2)40        assertTrue(scorePassword("0123456789") < 2)41    }4243    @Test fun mixed_8char_three_class_passes_create_gate() {44        // "short1A!" — lower+upper+digit+symbol, length 8 → "fair" (2).45        assertTrue(scorePassword("short1A!") >= 2)46    }4748    @Test fun mixed_12char_four_class_is_strong_and_passes_export_gate() {49        assertTrue(scorePassword("xK9#mQ2vL8wR") >= 3)50        assertTrue(scorePassword("Password123!") >= 3)51    }5253    @Test fun long_passphrase_is_strong() {54        assertEquals(4, scorePassword("correcthorsebatterystaple"))55    }5657    @Test fun score_is_always_in_range() {58        val samples = listOf(59            "", "a", "ab", "abc123", "Tr0ub4dor&3", "                ",60            "P@ssw0rd!2024xyz", "hunter2", "my secret diary 2024",61        )62        for (pw in samples) {63            val s = scorePassword(pw)64            assertTrue("score for '$pw' = $s out of range", s in 0..4)65        }66    }6768    @Test fun labels_cover_full_range_and_clamp() {69        assertEquals("very weak", strengthLabel(0))70        assertEquals("weak", strengthLabel(1))71        assertEquals("fair", strengthLabel(2))72        assertEquals("good", strengthLabel(3))73        assertEquals("strong", strengthLabel(4))74        // Out-of-range inputs clamp rather than crash.75        assertEquals("very weak", strengthLabel(-1))76        assertEquals("strong", strengthLabel(99))77    }7879    @Test fun more_length_never_lowers_a_clean_password() {80        // Monotonic-ish sanity: extending a varied password should not drop it81        // below the create gate.82        val base = "Xk9#mQ2v"83        assertTrue(scorePassword(base + "L8wR") >= scorePassword(base))84    }85}