1 /* 2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.textclassifier 17 18 import android.content.Context 19 import androidx.collection.LruCache 20 import androidx.test.ext.junit.runners.AndroidJUnit4 21 import com.android.textclassifier.common.ModelFile 22 import com.android.textclassifier.common.ModelType 23 import com.android.textclassifier.common.TextClassifierSettings 24 import com.android.textclassifier.testing.FakeContextBuilder 25 import com.android.textclassifier.testing.TestingDeviceConfig 26 import com.android.textclassifier.utils.TextClassifierUtils 27 import com.google.android.textclassifier.AnnotatorModel 28 import org.junit.Assert 29 import org.junit.Assume 30 import org.junit.Before 31 import org.junit.Test 32 import org.junit.runner.RunWith 33 import org.mockito.ArgumentMatchers 34 import org.mockito.ArgumentMatchers.any 35 import org.mockito.Mock 36 import org.mockito.Mockito 37 import org.mockito.MockitoAnnotations 38 39 @RunWith(AndroidJUnit4::class) 40 class TextClassifierOtpHelperTest { 41 @Mock 42 private lateinit var modelFileManager: ModelFileManager 43 44 private lateinit var context: Context 45 private lateinit var deviceConfig: TestingDeviceConfig 46 private lateinit var settings: TextClassifierSettings 47 private lateinit var annotatorModelCache: LruCache<ModelFile, AnnotatorModel> 48 private lateinit var tcImpl: TextClassifierImpl 49 50 @Before setupnull51 fun setup() { 52 Assume.assumeTrue(TextClassifierUtils.isOtpClassificationEnabled()) 53 54 MockitoAnnotations.initMocks(this) 55 this.context = 56 FakeContextBuilder() 57 .setAllIntentComponent(FakeContextBuilder.DEFAULT_COMPONENT) 58 .setAppLabel(FakeContextBuilder.DEFAULT_COMPONENT.packageName, "Test app") 59 .build() 60 this.deviceConfig = TestingDeviceConfig() 61 this.settings = TextClassifierSettings(deviceConfig, /* isWear= */ false) 62 this.annotatorModelCache = LruCache(2) 63 this.tcImpl = 64 TextClassifierImpl(context, settings, modelFileManager, annotatorModelCache) 65 66 Mockito.`when`( 67 modelFileManager.findBestModelFile( 68 ArgumentMatchers.eq(ModelType.ANNOTATOR), 69 any(), 70 any() 71 ) 72 ) 73 .thenReturn(TestDataUtils.getTestAnnotatorModelFileWrapped()) 74 Mockito.`when`( 75 modelFileManager.findBestModelFile( 76 ArgumentMatchers.eq(ModelType.LANG_ID), 77 any(), 78 any() 79 ) 80 ) 81 .thenReturn(TestDataUtils.getLangIdModelFileWrapped()) 82 Mockito.`when`( 83 modelFileManager.findBestModelFile( 84 ArgumentMatchers.eq(ModelType.ACTIONS_SUGGESTIONS), 85 any(), 86 any() 87 ) 88 ) 89 .thenReturn(TestDataUtils.getTestActionsModelFileWrapped()) 90 } 91 containsOtpnull92 private fun containsOtp(text: String): Boolean { 93 return TextClassifierOtpHelper.containsOtp( 94 text, 95 this.tcImpl, 96 ) 97 } 98 99 @Test testOtpDetectionnull100 fun testOtpDetection() { 101 Assert.assertFalse(containsOtp("hello")) 102 Assert.assertTrue(containsOtp("Your OTP code is 123456")) 103 } 104 105 @Test testContainsOtpLikePattern_lengthnull106 fun testContainsOtpLikePattern_length() { 107 val tooShortAlphaNum = "123G" 108 val tooShortNumOnly = "123" 109 val minLenAlphaNum = "123G5" 110 val minLenNumOnly = "1235" 111 val twoTriplets = "123 456" 112 val tooShortTriplets = "12 345" 113 val maxLen = "123456F8" 114 val tooLong = "123T56789" 115 116 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(minLenAlphaNum)) 117 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(minLenNumOnly)) 118 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(maxLen)) 119 Assert.assertFalse( 120 "$tooShortAlphaNum is too short", 121 TextClassifierOtpHelper.containsOtpLikePattern(tooShortAlphaNum) 122 ) 123 Assert.assertFalse( 124 "$tooShortNumOnly is too short", 125 TextClassifierOtpHelper.containsOtpLikePattern(tooShortNumOnly) 126 ) 127 Assert.assertFalse( 128 "$tooLong is too long", 129 TextClassifierOtpHelper.containsOtpLikePattern(tooLong) 130 ) 131 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(twoTriplets)) 132 Assert.assertFalse( 133 "$tooShortTriplets is too short", 134 TextClassifierOtpHelper.containsOtpLikePattern(tooShortTriplets) 135 ) 136 } 137 138 @Test testContainsOtpLikePattern_acceptsNonRomanAlphabeticalCharsnull139 fun testContainsOtpLikePattern_acceptsNonRomanAlphabeticalChars() { 140 val lowercase = "123ķ4" 141 val uppercase = "123Ŀ4" 142 val ideographicInMiddle = "123码456" 143 144 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(lowercase)) 145 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(uppercase)) 146 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(ideographicInMiddle)) 147 } 148 149 @Test testContainsOtpLikePattern_dashesnull150 fun testContainsOtpLikePattern_dashes() { 151 val oneDash = "G-3d523" 152 val manyDashes = "G-FD-745" 153 val tooManyDashes = "6--7893" 154 val oopsAllDashes = "------" 155 156 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(oneDash)) 157 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(oneDash)) 158 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(manyDashes)) 159 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(tooManyDashes)) 160 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(oopsAllDashes)) 161 } 162 163 @Test testContainsOtpLikePattern_lookaheadMustBeOtpCharnull164 fun testContainsOtpLikePattern_lookaheadMustBeOtpChar() { 165 val validLookahead = "g4zy75" 166 val spaceLookahead = "GVRXY 2" 167 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(validLookahead)) 168 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(spaceLookahead)) 169 } 170 171 @Test testContainsOtpLikePattern_dateExclusionnull172 fun testContainsOtpLikePattern_dateExclusion() { 173 val date = "01-01-2001" 174 val singleDigitDate = "1-1-2001" 175 val twoDigitYear = "1-1-01" 176 val dateWithOtpAfter = "1-1-01 is the date of your code T3425" 177 val dateWithOtpBefore = "your code 54-234-3 was sent on 1-1-01" 178 val otpWithDashesButInvalidDate = "34-58-30" 179 val otpWithDashesButInvalidYear = "12-1-3089" 180 181 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(date)) 182 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(singleDigitDate)) 183 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(twoDigitYear)) 184 185 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(dateWithOtpAfter)) 186 Assert.assertTrue(TextClassifierOtpHelper.containsOtpLikePattern(dateWithOtpBefore)) 187 Assert.assertTrue( 188 TextClassifierOtpHelper.containsOtpLikePattern(otpWithDashesButInvalidDate) 189 ) 190 Assert.assertTrue( 191 TextClassifierOtpHelper.containsOtpLikePattern(otpWithDashesButInvalidYear) 192 ) 193 } 194 195 @Test testContainsOtpLikePattern_phoneExclusionnull196 fun testContainsOtpLikePattern_phoneExclusion() { 197 val parens = "(888) 8888888" 198 val allSpaces = "888 888 8888" 199 val withDash = "(888) 888-8888" 200 val allDashes = "888-888-8888" 201 val allDashesWithParen = "(888)-888-8888" 202 203 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(parens)) 204 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(allSpaces)) 205 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(withDash)) 206 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(allDashes)) 207 Assert.assertFalse(TextClassifierOtpHelper.containsOtpLikePattern(allDashesWithParen)) 208 } 209 210 @Test testContainsOtp_falsePositiveExclusionnull211 fun testContainsOtp_falsePositiveExclusion() { 212 // OTP: [888-8888] falsePositives=[] finalOtpCandidate=[1234] 213 Assert.assertTrue(containsOtp("Your OTP is 888-8888")) 214 215 // OTP: [1234, 888-8888] falsePositives=[(888) 888-8888] finalOtpCandidate=[1234] 216 Assert.assertTrue(containsOtp("1234 is your OTP, call (888) 888-8888 for more info")) 217 218 // OTP: [888-8888] falsePositives=[(888) 888-8888] finalOtpCandidate=[] 219 Assert.assertFalse(containsOtp("Your OTP can't be shared at this point, please call (888) 888-8888")) 220 221 // OTP: [1234, 01-01-2001] falsePositives=[01-01-2001] finalOtpCandidate=[1234] 222 Assert.assertTrue(containsOtp("Your OTP code is 1234 and this is sent on 01-01-2001")) 223 224 // OTP: [01-01-2001] falsePositives=[01-01-2001] finalOtpCandidate=[] 225 Assert.assertFalse(containsOtp("Your OTP code is null and this is sent on 01-01-2001")) 226 } 227 228 @Test testContainsOtp_mustHaveNumbernull229 fun testContainsOtp_mustHaveNumber() { 230 val noNums = "TEFHXES" 231 Assert.assertFalse(containsOtp(noNums)) 232 } 233 234 @Test testContainsOtp_startAndEndnull235 fun testContainsOtp_startAndEnd() { 236 val noSpaceStart = "your code isG-345821" 237 val noSpaceEnd = "your code is G-345821for real" 238 val numberSpaceStart = "your code is 4 G-345821" 239 val numberSpaceEnd = "your code is G-345821 3" 240 val colonStart = "your code is:G-345821" 241 val newLineStart = "your code is \nG-345821" 242 val quote = "your code is 'G-345821'" 243 val doubleQuote = "your code is \"G-345821\"" 244 val bracketStart = "your code is [G-345821" 245 val ideographicStart = "your code is码G-345821" 246 val colonStartNumberPreceding = "your code is4:G-345821" 247 val periodEnd = "you code is G-345821." 248 val parens = "you code is (G-345821)" 249 val squareBrkt = "you code is [G-345821]" 250 val dashEnd = "you code is 'G-345821-'" 251 val randomSymbolEnd = "your code is G-345821$" 252 val underscoreEnd = "you code is 'G-345821_'" 253 val ideographicEnd = "your code is码G-345821码" 254 Assert.assertFalse(containsOtp(noSpaceStart)) 255 Assert.assertFalse(containsOtp(noSpaceEnd)) 256 Assert.assertFalse(containsOtp(numberSpaceStart)) 257 Assert.assertFalse(containsOtp(numberSpaceEnd)) 258 Assert.assertFalse(containsOtp(colonStartNumberPreceding)) 259 Assert.assertFalse(containsOtp(dashEnd)) 260 Assert.assertFalse(containsOtp(underscoreEnd)) 261 Assert.assertFalse(containsOtp(randomSymbolEnd)) 262 Assert.assertTrue(containsOtp(colonStart)) 263 Assert.assertTrue(containsOtp(newLineStart)) 264 Assert.assertTrue(containsOtp(quote)) 265 Assert.assertTrue(containsOtp(doubleQuote)) 266 Assert.assertTrue(containsOtp(bracketStart)) 267 Assert.assertTrue(containsOtp(ideographicStart)) 268 Assert.assertTrue(containsOtp(periodEnd)) 269 Assert.assertTrue(containsOtp(parens)) 270 Assert.assertTrue(containsOtp(squareBrkt)) 271 Assert.assertTrue(containsOtp(ideographicEnd)) 272 } 273 274 @Test testContainsOtp_multipleFalsePositivesnull275 fun testContainsOtp_multipleFalsePositives() { 276 val otp = "code 1543 code" 277 val longFp = "888-777-6666" 278 val shortFp = "34ess" 279 val multipleLongFp = "$longFp something something $longFp" 280 val multipleLongFpWithOtpBefore = "$otp $multipleLongFp" 281 val multipleLongFpWithOtpAfter = "$multipleLongFp $otp" 282 val multipleLongFpWithOtpBetween = "$longFp $otp $longFp" 283 val multipleShortFp = "$shortFp something something $shortFp" 284 val multipleShortFpWithOtpBefore = "$otp $multipleShortFp" 285 val multipleShortFpWithOtpAfter = "$otp $multipleShortFp" 286 val multipleShortFpWithOtpBetween = "$shortFp $otp $shortFp" 287 Assert.assertFalse(containsOtp(multipleLongFp)) 288 Assert.assertFalse(containsOtp(multipleShortFp)) 289 Assert.assertTrue(containsOtp(multipleLongFpWithOtpBefore)) 290 Assert.assertTrue(containsOtp(multipleLongFpWithOtpAfter)) 291 Assert.assertTrue(containsOtp(multipleLongFpWithOtpBetween)) 292 Assert.assertTrue(containsOtp(multipleShortFpWithOtpBefore)) 293 Assert.assertTrue(containsOtp(multipleShortFpWithOtpAfter)) 294 Assert.assertTrue(containsOtp(multipleShortFpWithOtpBetween)) 295 } 296 297 @Test testContainsOtpCode_nonEnglishnull298 fun testContainsOtpCode_nonEnglish() { 299 val textWithOtp = "1234 是您的一次性代碼" // 1234 is your one time code 300 Assert.assertFalse(containsOtp(textWithOtp)) 301 } 302 303 @Test testContainsOtp_englishSpecificRegexnull304 fun testContainsOtp_englishSpecificRegex() { 305 val englishFalsePositive = "This is a false positive 4543" 306 val englishContextWords = 307 listOf( 308 "login", 309 "log in", 310 "2fa", 311 "authenticate", 312 "auth", 313 "authentication", 314 "tan", 315 "password", 316 "passcode", 317 "two factor", 318 "two-factor", 319 "2factor", 320 "2 factor", 321 "pin", 322 "one time", 323 ) 324 val englishContextWordsCase = listOf("LOGIN", "logIn", "LoGiN") 325 // Strings with a context word somewhere in the substring 326 val englishContextSubstrings = listOf("pins", "gaping", "backspin") 327 val codeInNextSentence = "context word: code. This sentence has the actual value of 434343" 328 val codeInNextSentenceTooFar = 329 "context word: code. ${"f".repeat(60)} This sentence has the actual value of 434343" 330 val codeTwoSentencesAfterContext = "context word: code. One sentence. actual value 34343" 331 val codeInSentenceBeforeContext = "34343 is a number. This number is a code" 332 val codeInSentenceAfterNewline = "your code is \n 34343" 333 val codeTooFarBeforeContext = "34343 ${"f".repeat(60)} code" 334 335 Assert.assertFalse(containsOtp(englishFalsePositive)) 336 for (context in englishContextWords) { 337 val englishTruePositive = "$context $englishFalsePositive" 338 Assert.assertTrue(containsOtp(englishTruePositive)) 339 } 340 for (context in englishContextWordsCase) { 341 val englishTruePositive = "$context $englishFalsePositive" 342 Assert.assertTrue(containsOtp(englishTruePositive)) 343 } 344 for (falseContext in englishContextSubstrings) { 345 val anotherFalsePositive = "$falseContext $englishFalsePositive" 346 Assert.assertFalse(containsOtp(anotherFalsePositive)) 347 } 348 Assert.assertTrue(containsOtp(codeInNextSentence)) 349 Assert.assertTrue(containsOtp(codeInSentenceAfterNewline)) 350 Assert.assertFalse(containsOtp(codeTwoSentencesAfterContext)) 351 Assert.assertFalse(containsOtp(codeInSentenceBeforeContext)) 352 Assert.assertFalse(containsOtp(codeInNextSentenceTooFar)) 353 Assert.assertFalse(containsOtp(codeTooFarBeforeContext)) 354 } 355 } 356