1 /** 2 * Copyright (C) 2023 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 17 package android.ext.services.notification 18 19 import android.app.Notification 20 import android.app.Notification.CATEGORY_EMAIL 21 import android.app.Notification.CATEGORY_MESSAGE 22 import android.app.Notification.CATEGORY_SOCIAL 23 import android.app.Notification.EXTRA_TEXT 24 import android.app.PendingIntent 25 import android.app.Person 26 import android.content.Context 27 import android.content.Intent 28 import android.icu.util.ULocale 29 import android.os.Build 30 import android.os.Build.VERSION.SDK_INT 31 import android.view.textclassifier.TextClassifier 32 import android.view.textclassifier.TextLanguage 33 import android.view.textclassifier.TextLinks 34 import androidx.test.core.app.ApplicationProvider 35 import androidx.test.ext.junit.runners.AndroidJUnit4 36 import com.google.common.truth.Truth.assertWithMessage 37 import org.junit.After 38 import org.junit.Assume.assumeTrue 39 import org.junit.Before 40 import org.junit.Test 41 import org.junit.runner.RunWith 42 import org.mockito.ArgumentMatchers.any 43 import org.mockito.Mockito 44 45 @RunWith(AndroidJUnit4::class) 46 class NotificationOtpDetectionHelperTest { 47 private val context = ApplicationProvider.getApplicationContext<Context>() 48 private val localeWithRegex = ULocale.ENGLISH 49 private val invalidLocale = ULocale.ROOT 50 51 private data class TestResult( 52 val expected: Boolean, 53 val actual: Boolean, 54 val failureMessage: String 55 ) 56 57 private val results = mutableListOf<TestResult>() 58 59 @Before enableFlagnull60 fun enableFlag() { 61 assumeTrue(SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) 62 results.clear() 63 } 64 65 @After verifyResultsnull66 fun verifyResults() { 67 val allFailuresMessage = StringBuilder("") 68 var numFailures = 0; 69 for ((expected, actual, failureMessage) in results) { 70 if (expected != actual) { 71 numFailures += 1 72 allFailuresMessage.append("$failureMessage\n") 73 } 74 } 75 assertWithMessage("Found $numFailures failures:\n$allFailuresMessage") 76 .that(numFailures).isEqualTo(0) 77 } 78 addResultnull79 private fun addResult(expected: Boolean, actual: Boolean, failureMessage: String) { 80 results.add(TestResult(expected, actual, failureMessage)) 81 } 82 83 @Test testGetTextForDetection_textFieldsIncludednull84 fun testGetTextForDetection_textFieldsIncluded() { 85 val text = "text" 86 val title = "title" 87 val subtext = "subtext" 88 val sensitive = NotificationOtpDetectionHelper.getTextForDetection( 89 createNotification(text = text, title = title, subtext = subtext)) 90 addResult(expected = true, sensitive.contains(text),"expected sensitive text to contain $text") 91 addResult(expected = true, sensitive.contains(title), "expected sensitive text to contain $title") 92 addResult(expected = true, sensitive.contains(subtext), "expected sensitive text to contain $subtext") 93 } 94 95 @Test testGetTextForDetection_nullTextFieldsnull96 fun testGetTextForDetection_nullTextFields() { 97 val text = "text" 98 val title = "title" 99 val subtext = "subtext" 100 var sensitive = NotificationOtpDetectionHelper.getTextForDetection( 101 createNotification(text = text, title = null, subtext = null)) 102 addResult(expected = true, sensitive.contains(text), "expected sensitive text to contain $text") 103 addResult(expected = false, sensitive.contains(title), "expected sensitive text not to contain $title") 104 addResult(expected = false, sensitive.contains("subtext"), "expected sensitive text not to contain $subtext") 105 sensitive = NotificationOtpDetectionHelper.getTextForDetection( 106 createNotification(text = null, title = null, subtext = null)) 107 addResult(expected = true, sensitive != null, "expected to get a nonnull string") 108 val nullExtras = createNotification(text = null, title = null, subtext = null).apply { 109 this.extras = null 110 } 111 sensitive = NotificationOtpDetectionHelper.getTextForDetection(nullExtras) 112 addResult(expected = true, sensitive != null, "expected to get a nonnull string") 113 } 114 115 @Test testGetTextForDetection_messagesIncludedSortednull116 fun testGetTextForDetection_messagesIncludedSorted() { 117 val empty = Person.Builder().setName("test name").build() 118 val messageText1 = "message text 1" 119 val messageText2 = "message text 2" 120 val messageText3 = "message text 3" 121 val timestamp1 = 0L 122 val timestamp2 = 1000L 123 val timestamp3 = 50L 124 val message1 = 125 Notification.MessagingStyle.Message(messageText1, 126 timestamp1, 127 empty) 128 val message2 = 129 Notification.MessagingStyle.Message(messageText2, 130 timestamp2, 131 empty) 132 val message3 = 133 Notification.MessagingStyle.Message(messageText3, 134 timestamp3, 135 empty) 136 val style = Notification.MessagingStyle(empty).apply { 137 addMessage(message1) 138 addMessage(message2) 139 addMessage(message3) 140 } 141 val notif = createNotification(style = style) 142 val sensitive = NotificationOtpDetectionHelper.getTextForDetection(notif) 143 addResult(expected = true, sensitive.contains(messageText1), "expected sensitive text to contain $messageText1") 144 addResult(expected = true, sensitive.contains(messageText2), "expected sensitive text to contain $messageText2") 145 addResult(expected = true, sensitive.contains(messageText3), "expected sensitive text to contain $messageText3") 146 147 // MessagingStyle notifications get their main text set automatically to their first 148 // message, so we should skip to the end of that to find the message text 149 val notifText = notif.extras.getCharSequence(EXTRA_TEXT)?.toString() ?: "" 150 val messagesSensitiveStartIdx = sensitive.indexOf(notifText) + notifText.length 151 val sensitiveSub = sensitive.substring(messagesSensitiveStartIdx) 152 val text1Position = sensitiveSub.indexOf(messageText1) 153 val text2Position = sensitiveSub.indexOf(messageText2) 154 val text3Position = sensitiveSub.indexOf(messageText3) 155 // The messages should be sorted by timestamp, newest first, so 2 -> 3 -> 1 156 addResult(expected = true, text2Position < text1Position, "expected the newest message (2) to be first in \"$sensitiveSub\"") 157 addResult(expected = true, text2Position < text3Position, "expected the newest message (2) to be first in \"$sensitiveSub\"") 158 addResult(expected = true, text3Position < text1Position, "expected the middle message (3) to be center in \"$sensitiveSub\"") 159 } 160 161 @Test testGetTextForDetection_textLinesIncludednull162 fun testGetTextForDetection_textLinesIncluded() { 163 val style = Notification.InboxStyle() 164 val extraLine = "extra line" 165 style.addLine(extraLine) 166 val sensitive = NotificationOtpDetectionHelper 167 .getTextForDetection(createNotification(style = style)) 168 addResult(expected = true, sensitive.contains(extraLine), "expected sensitive text to contain $extraLine") 169 } 170 171 @Test testGetTextForDetection_bigTextStyleTextsIncludednull172 fun testGetTextForDetection_bigTextStyleTextsIncluded() { 173 val style = Notification.BigTextStyle() 174 val bigText = "BIG TEXT" 175 val bigTitleText = "BIG TITLE TEXT" 176 val summaryText = "summary text" 177 style.bigText(bigText) 178 style.setBigContentTitle(bigTitleText) 179 style.setSummaryText(summaryText) 180 val sensitive = NotificationOtpDetectionHelper 181 .getTextForDetection(createNotification(style = style)) 182 addResult(expected = true, sensitive.contains(bigText), "expected sensitive text to contain $bigText") 183 addResult(expected = 184 true, 185 sensitive.contains(bigTitleText), 186 "expected sensitive text to contain $bigTitleText" 187 ) 188 addResult(expected = 189 true, 190 sensitive.contains(summaryText), 191 "expected sensitive text to contain $summaryText" 192 ) 193 } 194 195 @Test testGetTextForDetection_maxLennull196 fun testGetTextForDetection_maxLen() { 197 val text = "0123456789".repeat(70) // 700 chars 198 val sensitive = 199 NotificationOtpDetectionHelper.getTextForDetection(createNotification(text = text)) 200 addResult(expected = true, sensitive.length <= 600, "Expected to be 600 chars or fewer") 201 } 202 203 @Test testShouldCheckForOtp_stylesnull204 fun testShouldCheckForOtp_styles() { 205 val style = Notification.InboxStyle() 206 var shouldCheck = NotificationOtpDetectionHelper 207 .shouldCheckForOtp(createNotification(style = style)) 208 addResult(expected = true, shouldCheck, "InboxStyle should be checked") 209 val empty = Person.Builder().setName("test").build() 210 val style2 = Notification.MessagingStyle(empty) 211 val style3 = Notification.BigPictureStyle() 212 val rejectedStyle = Notification.MediaStyle() 213 shouldCheck = NotificationOtpDetectionHelper 214 .shouldCheckForOtp(createNotification(style = style2)) 215 addResult(expected = true, shouldCheck, "MessagingStyle should be checked") 216 shouldCheck = NotificationOtpDetectionHelper 217 .shouldCheckForOtp(createNotification()) 218 addResult(expected = false, shouldCheck, "No style should not be checked") 219 shouldCheck = NotificationOtpDetectionHelper 220 .shouldCheckForOtp(createNotification(style = style3)) 221 addResult(expected = false, shouldCheck, "Valid non-messaging non-inbox style should not be checked") 222 shouldCheck = NotificationOtpDetectionHelper 223 .shouldCheckForOtp(createNotification(text = "your one time code is 4343434", 224 style = rejectedStyle)) 225 addResult(expected = false, shouldCheck, "MediaStyle should always be rejected") 226 } 227 228 @Test testShouldCheckForOtp_categoriesnull229 fun testShouldCheckForOtp_categories() { 230 var shouldCheck = NotificationOtpDetectionHelper 231 .shouldCheckForOtp(createNotification(category = CATEGORY_MESSAGE)) 232 addResult(expected = true, shouldCheck, "$CATEGORY_MESSAGE should be checked") 233 shouldCheck = NotificationOtpDetectionHelper 234 .shouldCheckForOtp(createNotification(category = CATEGORY_SOCIAL)) 235 addResult(expected = true, shouldCheck, "$CATEGORY_SOCIAL should be checked") 236 shouldCheck = NotificationOtpDetectionHelper 237 .shouldCheckForOtp(createNotification(category = CATEGORY_EMAIL)) 238 addResult(expected = true, shouldCheck, "$CATEGORY_EMAIL should be checked") 239 shouldCheck = NotificationOtpDetectionHelper 240 .shouldCheckForOtp(createNotification(category = "")) 241 addResult(expected = false, shouldCheck, "Empty string category should not be checked") 242 } 243 244 @Test testShouldCheckForOtp_regexnull245 fun testShouldCheckForOtp_regex() { 246 val shouldCheck = NotificationOtpDetectionHelper 247 .shouldCheckForOtp(createNotification(text = "45454", category = "")) 248 assertWithMessage("Regex matches should be checked").that(shouldCheck).isTrue() 249 } 250 251 @Test testShouldCheckForOtp_publicVersionnull252 fun testShouldCheckForOtp_publicVersion() { 253 var publicVersion = createNotification(category = CATEGORY_MESSAGE) 254 var shouldCheck = NotificationOtpDetectionHelper 255 .shouldCheckForOtp(createNotification(publicVersion = publicVersion)) 256 257 addResult(expected = true, shouldCheck, "notifications with a checked category in their public version should " + 258 "be checked") 259 publicVersion = createNotification(style = Notification.InboxStyle()) 260 shouldCheck = NotificationOtpDetectionHelper 261 .shouldCheckForOtp(createNotification(publicVersion = publicVersion)) 262 addResult(expected = true, shouldCheck, "notifications with a checked style in their public version should " + 263 "be checked") 264 } 265 266 267 @Test testContainsOtp_lengthnull268 fun testContainsOtp_length() { 269 val tooShortAlphaNum = "123G" 270 val tooShortNumOnly = "123" 271 val minLenAlphaNum = "123G5" 272 val minLenNumOnly = "1235" 273 val twoTriplets = "123 456" 274 val tooShortTriplets = "12 345" 275 val maxLen = "123456F8" 276 val tooLong = "123T56789" 277 278 addMatcherTestResult(expected = true, minLenAlphaNum) 279 addMatcherTestResult(expected = true, minLenNumOnly) 280 addMatcherTestResult(expected = true, maxLen) 281 addMatcherTestResult(expected = false, tooShortAlphaNum, customFailureMessage = "is too short") 282 addMatcherTestResult(expected = false, tooShortNumOnly, customFailureMessage = "is too short") 283 addMatcherTestResult(expected = false, tooLong, customFailureMessage = "is too long") 284 addMatcherTestResult(expected = true, twoTriplets) 285 addMatcherTestResult(expected = false, tooShortTriplets, customFailureMessage = "is too short") 286 } 287 288 @Test testContainsOtp_acceptsNonRomanAlphabeticalCharsnull289 fun testContainsOtp_acceptsNonRomanAlphabeticalChars() { 290 val lowercase = "123ķ4" 291 val uppercase = "123Ŀ4" 292 val ideographicInMiddle = "123码456" 293 addMatcherTestResult(expected = true, lowercase) 294 addMatcherTestResult(expected = true, uppercase) 295 addMatcherTestResult(expected = false, ideographicInMiddle) 296 } 297 298 @Test testContainsOtp_mustHaveNumbernull299 fun testContainsOtp_mustHaveNumber() { 300 val noNums = "TEFHXES" 301 addMatcherTestResult(expected = false, noNums) 302 } 303 304 @Test testContainsOtp_dateExclusionnull305 fun testContainsOtp_dateExclusion() { 306 val date = "01-01-2001" 307 val singleDigitDate = "1-1-2001" 308 val twoDigitYear = "1-1-01" 309 val dateWithOtpAfter = "1-1-01 is the date of your code T3425" 310 val dateWithOtpBefore = "your code 54-234-3 was sent on 1-1-01" 311 val otpWithDashesButInvalidDate = "34-58-30" 312 val otpWithDashesButInvalidYear = "12-1-3089" 313 314 addMatcherTestResult( 315 expected = true, 316 date, 317 checkForFalsePositives = false, 318 customFailureMessage = "should match if checkForFalsePositives is false" 319 ) 320 addMatcherTestResult( 321 expected = false, 322 date, 323 customFailureMessage = "should not match if checkForFalsePositives is true" 324 ) 325 addMatcherTestResult(expected = false, singleDigitDate) 326 addMatcherTestResult(expected = false, twoDigitYear) 327 addMatcherTestResult(expected = true, dateWithOtpAfter) 328 addMatcherTestResult(expected = true, dateWithOtpBefore) 329 addMatcherTestResult(expected = true, otpWithDashesButInvalidDate) 330 addMatcherTestResult(expected = true, otpWithDashesButInvalidYear) 331 } 332 333 @Test testContainsOtp_phoneExclusionnull334 fun testContainsOtp_phoneExclusion() { 335 val parens = "(888) 8888888" 336 val allSpaces = "888 888 8888" 337 val withDash = "(888) 888-8888" 338 val allDashes = "888-888-8888" 339 val allDashesWithParen = "(888)-888-8888" 340 addMatcherTestResult( 341 expected = true, 342 parens, 343 checkForFalsePositives = false, 344 customFailureMessage = "should match if checkForFalsePositives is false" 345 ) 346 addMatcherTestResult(expected = false, parens) 347 addMatcherTestResult(expected = false, allSpaces) 348 addMatcherTestResult(expected = false, withDash) 349 addMatcherTestResult(expected = false, allDashes) 350 addMatcherTestResult(expected = false, allDashesWithParen) 351 } 352 353 @Test testContainsOtp_dashesnull354 fun testContainsOtp_dashes() { 355 val oneDash = "G-3d523" 356 val manyDashes = "G-FD-745" 357 val tooManyDashes = "6--7893" 358 val oopsAllDashes = "------" 359 addMatcherTestResult(expected = true, oneDash) 360 addMatcherTestResult(expected = true, manyDashes) 361 addMatcherTestResult(expected = false, tooManyDashes) 362 addMatcherTestResult(expected = false, oopsAllDashes) 363 } 364 365 @Test testContainsOtp_startAndEndnull366 fun testContainsOtp_startAndEnd() { 367 val noSpaceStart = "your code isG-345821" 368 val noSpaceEnd = "your code is G-345821for real" 369 val numberSpaceStart = "your code is 4 G-345821" 370 val numberSpaceEnd = "your code is G-345821 3" 371 val colonStart = "your code is:G-345821" 372 val newLineStart = "your code is \nG-345821" 373 val quote = "your code is 'G-345821'" 374 val doubleQuote = "your code is \"G-345821\"" 375 val bracketStart = "your code is [G-345821" 376 val ideographicStart = "your code is码G-345821" 377 val colonStartNumberPreceding = "your code is4:G-345821" 378 val periodEnd = "you code is G-345821." 379 val parens = "you code is (G-345821)" 380 val squareBrkt = "you code is [G-345821]" 381 val dashEnd = "you code is 'G-345821-'" 382 val randomSymbolEnd = "your code is G-345821$" 383 val underscoreEnd = "you code is 'G-345821_'" 384 val ideographicEnd = "your code is码G-345821码" 385 addMatcherTestResult(expected = false, noSpaceStart) 386 addMatcherTestResult(expected = false, noSpaceEnd) 387 addMatcherTestResult(expected = false, numberSpaceStart) 388 addMatcherTestResult(expected = false, numberSpaceEnd) 389 addMatcherTestResult(expected = false, colonStartNumberPreceding) 390 addMatcherTestResult(expected = false, dashEnd) 391 addMatcherTestResult(expected = false, underscoreEnd) 392 addMatcherTestResult(expected = false, randomSymbolEnd) 393 addMatcherTestResult(expected = true, colonStart) 394 addMatcherTestResult(expected = true, newLineStart) 395 addMatcherTestResult(expected = true, quote) 396 addMatcherTestResult(expected = true, doubleQuote) 397 addMatcherTestResult(expected = true, bracketStart) 398 addMatcherTestResult(expected = true, ideographicStart) 399 addMatcherTestResult(expected = true, periodEnd) 400 addMatcherTestResult(expected = true, parens) 401 addMatcherTestResult(expected = true, squareBrkt) 402 addMatcherTestResult(expected = true, ideographicEnd) 403 } 404 405 @Test testContainsOtp_lookaheadMustBeOtpCharnull406 fun testContainsOtp_lookaheadMustBeOtpChar() { 407 val validLookahead = "g4zy75" 408 val spaceLookahead = "GVRXY 2" 409 addMatcherTestResult(expected = true, validLookahead) 410 addMatcherTestResult(expected = false, spaceLookahead) 411 } 412 413 @Test testContainsOtp_threeDontMatch_withoutLanguageSpecificRegexnull414 fun testContainsOtp_threeDontMatch_withoutLanguageSpecificRegex() { 415 val tc = getTestTextClassifier(invalidLocale) 416 val threeLowercase = "34agb" 417 addMatcherTestResult(expected = false, threeLowercase, textClassifier = tc) 418 } 419 420 @Test testContainsOtp_englishSpecificRegexnull421 fun testContainsOtp_englishSpecificRegex() { 422 val tc = getTestTextClassifier(ULocale.ENGLISH) 423 val englishFalsePositive = "This is a false positive 4543" 424 val englishContextWords = listOf("login", "log in", "2fa", "authenticate", "auth", 425 "authentication", "tan", "password", "passcode", "two factor", "two-factor", "2factor", 426 "2 factor", "pin", "one time") 427 val englishContextWordsCase = listOf("LOGIN", "logIn", "LoGiN") 428 // Strings with a context word somewhere in the substring 429 val englishContextSubstrings = listOf("pins", "gaping", "backspin") 430 val codeInNextSentence = "context word: code. This sentence has the actual value of 434343" 431 val codeInNextSentenceTooFar = 432 "context word: code. ${"f".repeat(60)} This sentence has the actual value of 434343" 433 val codeTwoSentencesAfterContext = "context word: code. One sentence. actual value 34343" 434 val codeInSentenceBeforeContext = "34343 is a number. This number is a code" 435 val codeInSentenceAfterNewline = "your code is \n 34343" 436 val codeTooFarBeforeContext = "34343 ${"f".repeat(60)} code" 437 438 addMatcherTestResult(expected = false, englishFalsePositive, textClassifier = tc) 439 for (context in englishContextWords) { 440 val englishTruePositive = "$context $englishFalsePositive" 441 addMatcherTestResult(expected = true, englishTruePositive, textClassifier = tc) 442 } 443 for (context in englishContextWordsCase) { 444 val englishTruePositive = "$context $englishFalsePositive" 445 addMatcherTestResult(expected = true, englishTruePositive, textClassifier = tc) 446 } 447 for (falseContext in englishContextSubstrings) { 448 val anotherFalsePositive = "$falseContext $englishFalsePositive" 449 addMatcherTestResult(expected = false, anotherFalsePositive, textClassifier = tc) 450 } 451 addMatcherTestResult(expected = true, codeInNextSentence, textClassifier = tc) 452 addMatcherTestResult(expected = true, codeInSentenceAfterNewline, textClassifier = tc) 453 addMatcherTestResult(expected = false, codeTwoSentencesAfterContext, textClassifier = tc) 454 addMatcherTestResult(expected = false, codeInSentenceBeforeContext, textClassifier = tc) 455 addMatcherTestResult(expected = false, codeInNextSentenceTooFar, textClassifier = tc) 456 addMatcherTestResult(expected = false, codeTooFarBeforeContext, textClassifier = tc) 457 } 458 459 @Test testContainsOtp_notificationFieldsCheckedIndividuallynull460 fun testContainsOtp_notificationFieldsCheckedIndividually() { 461 val tc = getTestTextClassifier(ULocale.ENGLISH) 462 // Together, the title and text will match the language-specific regex and the main regex, 463 // but apart, neither are enough 464 val notification = createNotification(text = "code", title = "434343") 465 addMatcherTestResult(expected = true, "code 434343") 466 addResult(expected = false, NotificationOtpDetectionHelper.containsOtp(notification, true, 467 tc), "Expected text of 'code' and title of '434343' not to match") 468 } 469 470 @Test testContainsOtp_multipleFalsePositivesnull471 fun testContainsOtp_multipleFalsePositives() { 472 val otp = "code 1543 code" 473 val longFp = "888-777-6666" 474 val shortFp = "34ess" 475 val multipleLongFp = "$longFp something something $longFp" 476 val multipleLongFpWithOtpBefore = "$otp $multipleLongFp" 477 val multipleLongFpWithOtpAfter = "$multipleLongFp $otp" 478 val multipleLongFpWithOtpBetween = "$longFp $otp $longFp" 479 val multipleShortFp = "$shortFp something something $shortFp" 480 val multipleShortFpWithOtpBefore = "$otp $multipleShortFp" 481 val multipleShortFpWithOtpAfter = "$otp $multipleShortFp" 482 val multipleShortFpWithOtpBetween = "$shortFp $otp $shortFp" 483 addMatcherTestResult(expected = false, multipleLongFp) 484 addMatcherTestResult(expected = false, multipleShortFp) 485 addMatcherTestResult(expected = true, multipleLongFpWithOtpBefore) 486 addMatcherTestResult(expected = true, multipleLongFpWithOtpAfter) 487 addMatcherTestResult(expected = true, multipleLongFpWithOtpBetween) 488 addMatcherTestResult(expected = true, multipleShortFpWithOtpBefore) 489 addMatcherTestResult(expected = true, multipleShortFpWithOtpAfter) 490 addMatcherTestResult(expected = true, multipleShortFpWithOtpBetween) 491 } 492 493 @Test testContainsOtpCode_falseIfNoLanguageSpecificRegexnull494 fun testContainsOtpCode_falseIfNoLanguageSpecificRegex() { 495 val tc = getTestTextClassifier(invalidLocale) 496 val text = "your one time code is 34343" 497 addMatcherTestResult(expected = false, text, textClassifier = tc) 498 } 499 500 @Test testContainsOtpCode_languageSpecificOverridesFalsePositivesExceptDatenull501 fun testContainsOtpCode_languageSpecificOverridesFalsePositivesExceptDate() { 502 // TC will detect an address, but the language-specific regex will be preferred 503 val tc = getTestTextClassifier(localeWithRegex, listOf(TextClassifier.TYPE_ADDRESS)) 504 val date = "1-1-01" 505 // Dates should still be checked 506 addMatcherTestResult(expected = false, date, textClassifier = tc) 507 // A string with a code with three lowercase letters, and an excluded year 508 val withOtherFalsePositives = "your login code is abd4f 1985" 509 // Other false positive regular expressions should not be checked 510 addMatcherTestResult(expected = true, withOtherFalsePositives, textClassifier = tc) 511 } 512 createNotificationnull513 private fun createNotification( 514 text: String? = "", 515 title: String? = "", 516 subtext: String? = "", 517 category: String? = "", 518 style: Notification.Style? = null, 519 publicVersion: Notification? = null 520 ): Notification { 521 val intent = Intent(Intent.ACTION_MAIN) 522 intent.setFlags( 523 Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP 524 or Intent.FLAG_ACTIVITY_CLEAR_TOP 525 ) 526 intent.setAction(Intent.ACTION_MAIN) 527 intent.setPackage(context.packageName) 528 529 val nb = Notification.Builder(context, "") 530 nb.setContentText(text) 531 nb.setContentTitle(title) 532 nb.setSubText(subtext) 533 nb.setCategory(category) 534 nb.setContentIntent(createTestPendingIntent()) 535 if (style != null) { 536 nb.setStyle(style) 537 } 538 if (publicVersion != null) { 539 nb.setPublicVersion(publicVersion) 540 } 541 return nb.build() 542 } 543 addMatcherTestResultnull544 private fun addMatcherTestResult( 545 expected: Boolean, 546 text: String, 547 checkForFalsePositives: Boolean = true, 548 textClassifier: TextClassifier? = null, 549 customFailureMessage: String? = null 550 ) { 551 val failureMessage = if (customFailureMessage != null) { 552 "$text $customFailureMessage" 553 } else if (expected) { 554 "$text should match" 555 } else { 556 "$text should not match" 557 } 558 addResult(expected = expected, NotificationOtpDetectionHelper.containsOtp( 559 createNotification(text), checkForFalsePositives, textClassifier), failureMessage) 560 } 561 createTestPendingIntentnull562 private fun createTestPendingIntent(): PendingIntent { 563 val intent = Intent(Intent.ACTION_MAIN) 564 intent.setFlags( 565 Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP 566 or Intent.FLAG_ACTIVITY_CLEAR_TOP 567 ) 568 intent.setAction(Intent.ACTION_MAIN) 569 intent.setPackage(context.packageName) 570 571 return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_MUTABLE) 572 } 573 574 // Creates a mock TextClassifier that will report back that text provided to it matches the 575 // given language codes (for language requests) and textClassifier entities (for links request) getTestTextClassifiernull576 private fun getTestTextClassifier( 577 locale: ULocale?, 578 tcEntities: List<String>? = null 579 ): TextClassifier { 580 val tc = Mockito.mock(TextClassifier::class.java) 581 if (locale != null) { 582 Mockito.doReturn( 583 TextLanguage.Builder().putLocale(locale, 0.9f).build() 584 ).`when`(tc).detectLanguage(any(TextLanguage.Request::class.java)) 585 } 586 587 val entityMap = mutableMapOf<String, Float>() 588 // to build the TextLinks, the entity map must have at least one item 589 entityMap[TextClassifier.TYPE_URL] = 0.01f 590 for (entity in tcEntities ?: emptyList()) { 591 entityMap[entity] = 0.9f 592 } 593 Mockito.doReturn( 594 TextLinks.Builder("").addLink(0, 1, entityMap) 595 .build() 596 ).`when`(tc).generateLinks(any(TextLinks.Request::class.java)) 597 return tc 598 } 599 } 600