• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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