package com.peachpasswords.autofill import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test /** * OTP / 2FA field detection. The autofill framework only sets the `otp` * hint when the developer opts in; most don\'t, so the parser falls back to * a regex over the field\'s id/hint/text. The regex has to catch the labels * real 2FA forms actually use without mis-matching promo / coupon / discount * code fields. */ class AssistStructureOtpRegexTest { private val rx = AssistStructureParser.OTP_HAYSTACK_RX @Test fun matches_textbook_2fa_labels() { // The original textbook set the regex already covered. assertTrue(rx.containsMatchIn("otp")) assertTrue(rx.containsMatchIn("2fa")) assertTrue(rx.containsMatchIn("mfa")) assertTrue(rx.containsMatchIn("one-time password")) assertTrue(rx.containsMatchIn("verification code")) assertTrue(rx.containsMatchIn("auth code")) } @Test fun does_not_match_card_number_label() { assertFalse(rx.containsMatchIn("card number")) } @Test fun matches_labels_real_forms_use() { // These were silent misses before the widening. assertTrue(rx.containsMatchIn("verification")) assertTrue(rx.containsMatchIn("authentication")) assertTrue(rx.containsMatchIn("security code")) assertTrue(rx.containsMatchIn("token")) assertTrue(rx.containsMatchIn("confirm")) } @Test fun does_not_match_unrelated_code_fields() { // A standalone "code" alone is intentionally absent: promo / coupon // / discount fields all use it. Matching them would mis-fill with a // TOTP on signup / checkout forms. assertFalse(rx.containsMatchIn("code")) assertFalse(rx.containsMatchIn("promo code")) assertFalse(rx.containsMatchIn("coupon code")) assertFalse(rx.containsMatchIn("discount code")) assertFalse(rx.containsMatchIn("zip code")) assertFalse(rx.containsMatchIn("area code")) } @Test fun measures_realistic_labelled_form_corpus() { val otpLabels = listOf( "Verification code", "One-time code", "Enter the 6-digit code", "Authenticator code", "Security code", ) val unrelatedNumericLabels = listOf( "CVV", "ZIP code", "Card number", "Amount", "Quantity", "Postcode", "Date of birth", "PIN", ) // classify() matches against a lowercased haystack, so the corpus has to // be lowercased here too or every capitalised label looks like a miss. val otpMisses = otpLabels.filterNot { rx.containsMatchIn(it.lowercase()) } val falsePositives = unrelatedNumericLabels.filter { rx.containsMatchIn(it.lowercase()) } // PB-26-153's risk is an over-matched numeric field receiving a TOTP, so // the asserted criterion is the false-positive rate over unrelated // labels. Detection gaps are recorded, not asserted away: the two known // misses below are the measured behaviour of the live heuristic, and any // NEW miss fails this test rather than passing unnoticed. val knownGaps = setOf("Enter the 6-digit code", "Authenticator code") assertTrue("Unrelated labels classified as OTP: $falsePositives", falsePositives.isEmpty()) assertTrue( "New OTP detection gap: ${otpMisses.filterNot { it in knownGaps }}", otpMisses.all { it in knownGaps }, ) } }