33import static android .os .Looper .getMainLooper ;
44import static org .junit .Assert .assertEquals ;
55import static org .junit .Assert .assertNotNull ;
6- import static org .junit .Assert .assertNull ;
7- import static org .mockito .ArgumentMatchers .any ;
86import static org .mockito .ArgumentMatchers .anyString ;
9- import static org .mockito .ArgumentMatchers .eq ;
107import static org .mockito .Mockito .doAnswer ;
118import static org .mockito .Mockito .doReturn ;
129import static org .mockito .Mockito .mock ;
2522import org .junit .After ;
2623import org .junit .Before ;
2724import org .junit .Test ;
28- import org .mockito .ArgumentCaptor ;
2925
3026import java .io .IOException ;
3127import java .util .concurrent .CountDownLatch ;
3733
3834/**
3935 * Security tests for TOCTOU (Time-Of-Check-Time-Of-Use) vulnerabilities in auth flow.
40- *
36+ *
4137 * These tests verify that credentials cannot be swapped mid-flight between storage
4238 * and usage, preventing user-controlled bypass attacks.
4339 */
@@ -56,7 +52,7 @@ public void setUp() throws IOException {
5652 dispatcher = new PathBasedQueueDispatcher ();
5753 server .setDispatcher (dispatcher );
5854 IterableApi .overrideURLEndpointPath (server .url ("" ).toString ());
59-
55+
6056 mockKeychain = mock (IterableKeychain .class );
6157 }
6258
@@ -93,22 +89,22 @@ private void initIterableWithoutAuth() {
9389 @ Test
9490 public void testCompleteUserLogin_WithJWTAuth_NoToken_SkipsSensitiveOps () throws Exception {
9591 initIterableWithAuth ();
96-
92+
9793 // Spy on the API instance to verify method calls
9894 IterableApi api = spy (IterableApi .getInstance ());
9995 IterableApi .sharedInstance = api ;
100-
96+
10197 IterableInAppManager mockInAppManager = mock (IterableInAppManager .class );
10298 IterableEmbeddedManager mockEmbeddedManager = mock (IterableEmbeddedManager .class );
10399 when (api .getInAppManager ()).thenReturn (mockInAppManager );
104100 when (api .getEmbeddedManager ()).thenReturn (mockEmbeddedManager );
105-
106- // Directly call setAuthToken with null and bypassAuth=true to simulate
101+
102+ // Directly call setAuthToken with null and bypassAuth=true to simulate
107103 // attempting to bypass with no token (user-controlled bypass scenario)
108104 api .setAuthToken (null , true );
109-
105+
110106 shadowOf (getMainLooper ()).idle ();
111-
107+
112108 // Verify sensitive operations were NOT called (JWT auth enabled, no token)
113109 verify (mockInAppManager , never ()).syncInApp ();
114110 verify (mockEmbeddedManager , never ()).syncMessages ();
@@ -121,25 +117,25 @@ public void testCompleteUserLogin_WithJWTAuth_NoToken_SkipsSensitiveOps() throws
121117 @ Test
122118 public void testCompleteUserLogin_WithJWTAuth_WithToken_ExecutesSensitiveOps () throws Exception {
123119 initIterableWithAuth ();
124-
120+
125121 dispatcher .enqueueResponse ("/users/update" , new MockResponse ().setResponseCode (200 ).setBody ("{}" ));
126-
122+
127123 doReturn (validJWT ).when (authHandler ).onAuthTokenRequested ();
128-
124+
129125 // Spy on the API instance to verify method calls
130126 IterableApi api = spy (IterableApi .getInstance ());
131127 IterableApi .sharedInstance = api ;
132-
128+
133129 IterableInAppManager mockInAppManager = mock (IterableInAppManager .class );
134130 IterableEmbeddedManager mockEmbeddedManager = mock (IterableEmbeddedManager .class );
135131 when (api .getInAppManager ()).thenReturn (mockInAppManager );
136132 when (api .getEmbeddedManager ()).thenReturn (mockEmbeddedManager );
137-
133+
138134 api .
setEmail (
"[email protected] " );
139-
135+
140136 server .takeRequest (1 , TimeUnit .SECONDS );
141137 shadowOf (getMainLooper ()).idle ();
142-
138+
143139 // Verify sensitive operations WERE called with valid token
144140 verify (mockInAppManager ).syncInApp ();
145141 verify (mockEmbeddedManager ).syncMessages ();
@@ -152,20 +148,20 @@ public void testCompleteUserLogin_WithJWTAuth_WithToken_ExecutesSensitiveOps() t
152148 @ Test
153149 public void testCompleteUserLogin_WithoutJWTAuth_NoToken_ExecutesSensitiveOps () throws Exception {
154150 initIterableWithoutAuth ();
155-
151+
156152 // Spy on the API instance to verify method calls
157153 IterableApi api = spy (IterableApi .getInstance ());
158154 IterableApi .sharedInstance = api ;
159-
155+
160156 IterableInAppManager mockInAppManager = mock (IterableInAppManager .class );
161157 IterableEmbeddedManager mockEmbeddedManager = mock (IterableEmbeddedManager .class );
162158 when (api .getInAppManager ()).thenReturn (mockInAppManager );
163159 when (api .getEmbeddedManager ()).thenReturn (mockEmbeddedManager );
164-
160+
165161 api .
setEmail (
"[email protected] " );
166-
162+
167163 shadowOf (getMainLooper ()).idle ();
168-
164+
169165 // Verify sensitive operations WERE called (no JWT auth required)
170166 verify (mockInAppManager ).syncInApp ();
171167 verify (mockEmbeddedManager ).syncMessages ();
@@ -178,13 +174,13 @@ public void testCompleteUserLogin_WithoutJWTAuth_NoToken_ExecutesSensitiveOps()
178174 @ Test
179175 public void testStoreAuthData_CompletionHandler_ReceivesStoredCredentials () throws Exception {
180176 initIterableWithAuth ();
181-
177+
182178 IterableApi api = IterableApi .getInstance ();
183-
179+
184180 // Use reflection to spy on storeAuthData behavior
185181 IterableApi spyApi = spy (api );
186182 IterableApi .sharedInstance = spyApi ;
187-
183+
188184 // Set up mock keychain that attempts to swap credentials mid-flight
189185 doAnswer (invocation -> {
190186 // Malicious keychain that tries to swap email after storage
@@ -193,17 +189,17 @@ public void testStoreAuthData_CompletionHandler_ReceivesStoredCredentials() thro
193189 // Simulate attacker trying to modify keychain after storage
194190 return null ;
195191 }).when (mockKeychain ).saveEmail (anyString ());
196-
192+
197193 when (spyApi .getKeychain ()).thenReturn (mockKeychain );
198-
194+
199195 dispatcher .enqueueResponse ("/users/update" , new MockResponse ().setResponseCode (200 ).setBody ("{}" ));
200196 doReturn (validJWT ).when (authHandler ).onAuthTokenRequested ();
201-
197+
202198 final String originalEmail =
"[email protected] " ;
203199 final AtomicReference <String > completionHandlerEmail = new AtomicReference <>();
204200 final AtomicReference <String > completionHandlerToken = new AtomicReference <>();
205201 final CountDownLatch latch = new CountDownLatch (1 );
206-
202+
207203 // Capture what the completion handler receives
208204 spyApi .setEmail (originalEmail , new IterableHelper .SuccessHandler () {
209205 @ Override
@@ -212,11 +208,11 @@ public void onSuccess(JSONObject data) {
212208 latch .countDown ();
213209 }
214210 }, null );
215-
211+
216212 server .takeRequest (1 , TimeUnit .SECONDS );
217213 shadowOf (getMainLooper ()).idle ();
218214 latch .await (2 , TimeUnit .SECONDS );
219-
215+
220216 // Verify the API instance has the correct email
221217 assertEquals ("Email should match original" , originalEmail , spyApi .getEmail ());
222218 assertNotNull ("AuthToken should be set" , spyApi .getAuthToken ());
@@ -229,32 +225,32 @@ public void onSuccess(JSONObject data) {
229225 @ Test
230226 public void testSetAuthToken_UsesCompletionHandlerPattern () throws Exception {
231227 initIterableWithAuth ();
232-
228+
233229 dispatcher .enqueueResponse ("/users/update" , new MockResponse ().setResponseCode (200 ).setBody ("{}" ));
234230 doReturn (validJWT ).when (authHandler ).onAuthTokenRequested ();
235-
231+
236232 IterableApi api = spy (IterableApi .getInstance ());
237233 IterableApi .sharedInstance = api ;
238-
234+
239235 IterableInAppManager mockInAppManager = mock (IterableInAppManager .class );
240236 IterableEmbeddedManager mockEmbeddedManager = mock (IterableEmbeddedManager .class );
241237 when (api .getInAppManager ()).thenReturn (mockInAppManager );
242238 when (api .getEmbeddedManager ()).thenReturn (mockEmbeddedManager );
243-
239+
244240 // First set user
245241 api .
setEmail (
"[email protected] " );
246242 server .takeRequest (1 , TimeUnit .SECONDS );
247243 shadowOf (getMainLooper ()).idle ();
248-
244+
249245 // Clear previous invocations
250246 org .mockito .Mockito .clearInvocations (mockInAppManager , mockEmbeddedManager );
251-
247+
252248 // Now update auth token (simulating token refresh)
253249 final String newToken = "new_jwt_token_here" ;
254250 api .setAuthToken (newToken , false );
255-
251+
256252 shadowOf (getMainLooper ()).idle ();
257-
253+
258254 // Verify sensitive operations were called with updated token
259255 verify (mockInAppManager ).syncInApp ();
260256 verify (mockEmbeddedManager ).syncMessages ();
@@ -268,20 +264,20 @@ public void testSetAuthToken_UsesCompletionHandlerPattern() throws Exception {
268264 @ Test
269265 public void testSetAuthToken_BypassAuth_StillValidatesToken () throws Exception {
270266 initIterableWithAuth ();
271-
267+
272268 IterableApi api = spy (IterableApi .getInstance ());
273269 IterableApi .sharedInstance = api ;
274-
270+
275271 IterableInAppManager mockInAppManager = mock (IterableInAppManager .class );
276272 IterableEmbeddedManager mockEmbeddedManager = mock (IterableEmbeddedManager .class );
277273 when (api .getInAppManager ()).thenReturn (mockInAppManager );
278274 when (api .getEmbeddedManager ()).thenReturn (mockEmbeddedManager );
279-
275+
280276 // Try to bypass with no token set
281277 api .setAuthToken (null , true );
282-
278+
283279 shadowOf (getMainLooper ()).idle ();
284-
280+
285281 // Verify sensitive operations were NOT called (JWT auth enabled, no token)
286282 verify (mockInAppManager , never ()).syncInApp ();
287283 verify (mockEmbeddedManager , never ()).syncMessages ();
@@ -294,32 +290,32 @@ public void testSetAuthToken_BypassAuth_StillValidatesToken() throws Exception {
294290 @ Test
295291 public void testCredentialConsistency_StorageToUsage () throws Exception {
296292 initIterableWithAuth ();
297-
293+
298294 dispatcher .enqueueResponse ("/users/update" , new MockResponse ().setResponseCode (200 ).setBody ("{}" ));
299-
295+
300296 final String testEmail =
"[email protected] " ;
301297 doReturn (validJWT ).when (authHandler ).onAuthTokenRequested ();
302-
298+
303299 IterableApi api = IterableApi .getInstance ();
304-
300+
305301 // Set email
306302 api .setEmail (testEmail );
307303 server .takeRequest (1 , TimeUnit .SECONDS );
308304 shadowOf (getMainLooper ()).idle ();
309-
305+
310306 // Verify credentials match exactly
311307 assertEquals ("Email should match" , testEmail , api .getEmail ());
312308 assertEquals ("AuthToken should match" , validJWT , api .getAuthToken ());
313-
309+
314310 // Now test with userId
315311 dispatcher .enqueueResponse ("/users/update" , new MockResponse ().setResponseCode (200 ).setBody ("{}" ));
316312 final String testUserId = "user123" ;
317313 doReturn (validJWT ).when (authHandler ).onAuthTokenRequested ();
318-
314+
319315 api .setUserId (testUserId );
320316 server .takeRequest (1 , TimeUnit .SECONDS );
321317 shadowOf (getMainLooper ()).idle ();
322-
318+
323319 // Verify userId matches
324320 assertEquals ("UserId should match" , testUserId , api .getUserId ());
325321 assertEquals ("AuthToken should still match" , validJWT , api .getAuthToken ());
@@ -339,20 +335,20 @@ public void testStaleKeychainCredentials_NoToken_SkipsSensitiveOps() throws Exce
339335 editor .putString (IterableConstants .SHARED_PREFS_USERID_KEY , "staleUserId" );
340336 // No auth token stored
341337 editor .apply ();
342-
338+
343339 initIterableWithAuth ();
344-
340+
345341 IterableApi api = spy (IterableApi .getInstance ());
346342 IterableApi .sharedInstance = api ;
347-
343+
348344 IterableInAppManager mockInAppManager = mock (IterableInAppManager .class );
349345 IterableEmbeddedManager mockEmbeddedManager = mock (IterableEmbeddedManager .class );
350346 when (api .getInAppManager ()).thenReturn (mockInAppManager );
351347 when (api .getEmbeddedManager ()).thenReturn (mockEmbeddedManager );
352-
348+
353349 // Trigger initialization flow
354350 shadowOf (getMainLooper ()).idle ();
355-
351+
356352 // Verify sensitive operations were NOT called with stale credentials
357353 verify (mockInAppManager , never ()).syncInApp ();
358354 verify (mockEmbeddedManager , never ()).syncMessages ();
0 commit comments