AssistStructureOtpRegexTest.kt

3.6 KB · raw

1package com.peachpasswords.autofill23import org.junit.Assert.assertFalse4import org.junit.Assert.assertTrue5import org.junit.Test67/**8 * OTP / 2FA field detection. The autofill framework only sets the `otp`9 * hint when the developer opts in; most don\'t, so the parser falls back to10 * a regex over the field\'s id/hint/text. The regex has to catch the labels11 * real 2FA forms actually use without mis-matching promo / coupon / discount12 * code fields.13 */14class AssistStructureOtpRegexTest {1516    private val rx = AssistStructureParser.OTP_HAYSTACK_RX1718    @Test fun matches_textbook_2fa_labels() {19        // The original textbook set the regex already covered.20        assertTrue(rx.containsMatchIn("otp"))21        assertTrue(rx.containsMatchIn("2fa"))22        assertTrue(rx.containsMatchIn("mfa"))23        assertTrue(rx.containsMatchIn("one-time password"))24        assertTrue(rx.containsMatchIn("verification code"))25        assertTrue(rx.containsMatchIn("auth code"))26    }2728    @Test fun does_not_match_card_number_label() {29        assertFalse(rx.containsMatchIn("card number"))30    }3132    @Test fun matches_labels_real_forms_use() {33        // These were silent misses before the widening.34        assertTrue(rx.containsMatchIn("verification"))35        assertTrue(rx.containsMatchIn("authentication"))36        assertTrue(rx.containsMatchIn("security code"))37        assertTrue(rx.containsMatchIn("token"))38        assertTrue(rx.containsMatchIn("confirm"))39    }4041    @Test fun does_not_match_unrelated_code_fields() {42        // A standalone "code" alone is intentionally absent: promo / coupon43        // / discount fields all use it. Matching them would mis-fill with a44        // TOTP on signup / checkout forms.45        assertFalse(rx.containsMatchIn("code"))46        assertFalse(rx.containsMatchIn("promo code"))47        assertFalse(rx.containsMatchIn("coupon code"))48        assertFalse(rx.containsMatchIn("discount code"))49        assertFalse(rx.containsMatchIn("zip code"))50        assertFalse(rx.containsMatchIn("area code"))51    }5253    @Test fun measures_realistic_labelled_form_corpus() {54        val otpLabels = listOf(55            "Verification code",56            "One-time code",57            "Enter the 6-digit code",58            "Authenticator code",59            "Security code",60        )61        val unrelatedNumericLabels = listOf(62            "CVV",63            "ZIP code",64            "Card number",65            "Amount",66            "Quantity",67            "Postcode",68            "Date of birth",69            "PIN",70        )7172        // classify() matches against a lowercased haystack, so the corpus has to73        // be lowercased here too or every capitalised label looks like a miss.74        val otpMisses = otpLabels.filterNot { rx.containsMatchIn(it.lowercase()) }75        val falsePositives = unrelatedNumericLabels.filter { rx.containsMatchIn(it.lowercase()) }7677        // PB-26-153's risk is an over-matched numeric field receiving a TOTP, so78        // the asserted criterion is the false-positive rate over unrelated79        // labels. Detection gaps are recorded, not asserted away: the two known80        // misses below are the measured behaviour of the live heuristic, and any81        // NEW miss fails this test rather than passing unnoticed.82        val knownGaps = setOf("Enter the 6-digit code", "Authenticator code")83        assertTrue("Unrelated labels classified as OTP: $falsePositives", falsePositives.isEmpty())84        assertTrue(85            "New OTP detection gap: ${otpMisses.filterNot { it in knownGaps }}",86            otpMisses.all { it in knownGaps },87        )88    }89}