1 // Copyright 2015 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net; 6 7 import static org.hamcrest.CoreMatchers.equalTo; 8 import static org.hamcrest.CoreMatchers.notNullValue; 9 import static org.hamcrest.CoreMatchers.nullValue; 10 import static org.junit.Assert.assertThat; 11 import static org.junit.Assert.fail; 12 import static org.mockito.ArgumentMatchers.any; 13 import static org.mockito.ArgumentMatchers.anyLong; 14 import static org.mockito.ArgumentMatchers.eq; 15 import static org.mockito.ArgumentMatchers.isNull; 16 import static org.mockito.Mockito.mock; 17 import static org.mockito.Mockito.times; 18 import static org.mockito.Mockito.verify; 19 import static org.mockito.Mockito.verifyNoMoreInteractions; 20 import static org.mockito.Mockito.when; 21 22 import android.accounts.Account; 23 import android.accounts.AccountManager; 24 import android.accounts.AccountManagerCallback; 25 import android.accounts.AccountManagerFuture; 26 import android.accounts.AuthenticatorException; 27 import android.accounts.OperationCanceledException; 28 import android.app.Activity; 29 import android.app.Application; 30 import android.content.BroadcastReceiver; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.os.Bundle; 34 import android.os.Handler; 35 36 import org.junit.Assert; 37 import org.junit.Before; 38 import org.junit.Rule; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 import org.mockito.ArgumentCaptor; 42 import org.mockito.Captor; 43 import org.mockito.Mock; 44 import org.mockito.MockitoAnnotations; 45 import org.robolectric.Robolectric; 46 import org.robolectric.RuntimeEnvironment; 47 import org.robolectric.annotation.Config; 48 import org.robolectric.annotation.Implementation; 49 import org.robolectric.annotation.Implements; 50 import org.robolectric.shadows.ShadowAccountManager; 51 import org.robolectric.shadows.ShadowApplication; 52 53 import org.chromium.base.ApplicationStatus; 54 import org.chromium.base.test.BaseRobolectricTestRunner; 55 import org.chromium.base.test.util.JniMocker; 56 import org.chromium.net.HttpNegotiateAuthenticator.GetAccountsCallback; 57 import org.chromium.net.HttpNegotiateAuthenticator.RequestData; 58 59 import java.io.IOException; 60 import java.util.List; 61 62 /** Robolectric tests for HttpNegotiateAuthenticator */ 63 @RunWith(BaseRobolectricTestRunner.class) 64 @Config( 65 manifest = Config.NONE, 66 shadows = {HttpNegotiateAuthenticatorTest.ExtendedShadowAccountManager.class}) 67 public class HttpNegotiateAuthenticatorTest { 68 /** 69 * User the AccountManager to inject a mock instance. 70 * Note: Shadow classes need to be public and static. 71 */ 72 @Implements(AccountManager.class) 73 public static class ExtendedShadowAccountManager extends ShadowAccountManager { 74 @Implementation get(Context context)75 public static AccountManager get(Context context) { 76 return sMockAccountManager; 77 } 78 } 79 80 @Rule public JniMocker mocker = new JniMocker(); 81 @Mock private static AccountManager sMockAccountManager; 82 @Mock private HttpNegotiateAuthenticator.Natives mAuthenticatorJniMock; 83 @Captor private ArgumentCaptor<AccountManagerCallback<Bundle>> mBundleCallbackCaptor; 84 @Captor private ArgumentCaptor<AccountManagerCallback<Account[]>> mAccountCallbackCaptor; 85 @Captor private ArgumentCaptor<Bundle> mBundleCaptor; 86 87 @Before setUp()88 public void setUp() { 89 MockitoAnnotations.initMocks(this); 90 mocker.mock(HttpNegotiateAuthenticatorJni.TEST_HOOKS, mAuthenticatorJniMock); 91 } 92 93 /** Test of {@link HttpNegotiateAuthenticator#getNextAuthToken} */ 94 @Test testGetNextAuthToken()95 public void testGetNextAuthToken() { 96 final String accountType = "Dummy_Account"; 97 HttpNegotiateAuthenticator authenticator = createAuthenticator(accountType); 98 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 99 100 authenticator.getNextAuthToken(0, "test_principal", "", true); 101 102 verify(sMockAccountManager) 103 .getAuthTokenByFeatures( 104 eq(accountType), 105 eq("SPNEGO:HOSTBASED:test_principal"), 106 eq(new String[] {"SPNEGO"}), 107 any(Activity.class), 108 (Bundle) isNull(), 109 mBundleCaptor.capture(), 110 mBundleCallbackCaptor.capture(), 111 any(Handler.class)); 112 113 assertThat( 114 "There is no existing context", 115 mBundleCaptor.getValue().get(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), 116 nullValue()); 117 assertThat( 118 "The existing token is empty", 119 mBundleCaptor.getValue().getString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN), 120 equalTo("")); 121 assertThat( 122 "Delegation is allowed", 123 mBundleCaptor.getValue().getBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE), 124 equalTo(true)); 125 assertThat( 126 "getAuthTokenByFeatures was called with a callback", 127 mBundleCallbackCaptor.getValue(), 128 notNullValue()); 129 } 130 131 /** 132 * Test of {@link HttpNegotiateAuthenticator#getNextAuthToken} without a visible activity. 133 * This emulates the behavior with WebView, where the application is a generic one and doesn't 134 * set up the ApplicationStatus the same way. 135 */ 136 @Test 137 @Config(application = Application.class) testGetNextAuthTokenWithoutActivity()138 public void testGetNextAuthTokenWithoutActivity() { 139 final String accountType = "Dummy_Account"; 140 final Account[] returnedAccount = {new Account("name", accountType)}; 141 HttpNegotiateAuthenticator authenticator = createAuthenticator(accountType); 142 143 authenticator.getNextAuthToken(1234, "test_principal", "", true); 144 145 Assert.assertNull(ApplicationStatus.getLastTrackedFocusedActivity()); 146 verify(sMockAccountManager) 147 .getAccountsByTypeAndFeatures( 148 eq(accountType), 149 eq(new String[] {"SPNEGO"}), 150 mAccountCallbackCaptor.capture(), 151 any(Handler.class)); 152 153 mAccountCallbackCaptor.getValue().run(makeFuture(returnedAccount)); 154 155 verify(sMockAccountManager) 156 .getAuthToken( 157 any(Account.class), 158 eq("SPNEGO:HOSTBASED:test_principal"), 159 mBundleCaptor.capture(), 160 eq(true), 161 any(HttpNegotiateAuthenticator.GetTokenCallback.class), 162 any(Handler.class)); 163 164 assertThat( 165 "There is no existing context", 166 mBundleCaptor.getValue().get(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), 167 nullValue()); 168 assertThat( 169 "The existing token is empty", 170 mBundleCaptor.getValue().getString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN), 171 equalTo("")); 172 assertThat( 173 "Delegation is allowed", 174 mBundleCaptor.getValue().getBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE), 175 equalTo(true)); 176 } 177 178 /** Tests the behavior of {@link HttpNegotiateAuthenticator.GetAccountsCallback} */ 179 @Test testGetAccountCallback()180 public void testGetAccountCallback() { 181 String type = "Dummy_Account"; 182 HttpNegotiateAuthenticator authenticator = createAuthenticator(type); 183 RequestData requestData = new RequestData(); 184 requestData.nativeResultObject = 42; 185 requestData.accountManager = sMockAccountManager; 186 GetAccountsCallback callback = authenticator.new GetAccountsCallback(requestData); 187 188 // Should fail because there are no accounts 189 callback.run(makeFuture(new Account[] {})); 190 verify(mAuthenticatorJniMock) 191 .setResult( 192 eq(42L), 193 eq(authenticator), 194 eq(NetError.ERR_MISSING_AUTH_CREDENTIALS), 195 (String) isNull()); 196 197 // Should succeed, for a single account we use it for the AccountManager#getAuthToken call. 198 Account testAccount = new Account("a", type); 199 callback.run(makeFuture(new Account[] {testAccount})); 200 verify(sMockAccountManager) 201 .getAuthToken( 202 eq(testAccount), 203 (String) isNull(), 204 (Bundle) isNull(), 205 eq(true), 206 any(HttpNegotiateAuthenticator.GetTokenCallback.class), 207 any(Handler.class)); 208 209 // Should fail because there is more than one account 210 callback.run(makeFuture(new Account[] {new Account("a", type), new Account("b", type)})); 211 verify(mAuthenticatorJniMock, times(2)) 212 .setResult( 213 eq(42L), 214 eq(authenticator), 215 eq(NetError.ERR_MISSING_AUTH_CREDENTIALS), 216 (String) isNull()); 217 } 218 219 /** 220 * Tests the behavior of {@link HttpNegotiateAuthenticator.GetTokenCallback} when the result it 221 * receives contains an intent rather than a token directly. 222 */ 223 @Test testGetTokenCallbackWithIntent()224 public void testGetTokenCallbackWithIntent() { 225 String type = "Dummy_Account"; 226 HttpNegotiateAuthenticator authenticator = createAuthenticator(type); 227 RequestData requestData = new RequestData(); 228 requestData.nativeResultObject = 42; 229 requestData.authTokenType = "foo"; 230 requestData.account = new Account("a", type); 231 requestData.accountManager = sMockAccountManager; 232 Bundle b = new Bundle(); 233 b.putParcelable(AccountManager.KEY_INTENT, new Intent()); 234 235 authenticator.new GetTokenCallback(requestData).run(makeFuture(b)); 236 verifyNoMoreInteractions(sMockAccountManager); 237 238 // Verify that the broadcast receiver is registered 239 Intent intent = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); 240 ShadowApplication shadowApplication = ShadowApplication.getInstance(); 241 List<BroadcastReceiver> receivers = shadowApplication.getReceiversForIntent(intent); 242 assertThat("There is one registered broadcast receiver", receivers.size(), equalTo(1)); 243 244 // Send the intent to the receiver. 245 BroadcastReceiver receiver = receivers.get(0); 246 receiver.onReceive(RuntimeEnvironment.application.getApplicationContext(), intent); 247 248 // Verify that the auth token is properly requested from the account manager. 249 verify(sMockAccountManager) 250 .getAuthToken( 251 eq(new Account("a", type)), 252 eq("foo"), 253 (Bundle) isNull(), 254 eq(true), 255 any(HttpNegotiateAuthenticator.GetTokenCallback.class), 256 (Handler) isNull()); 257 } 258 259 /** Test of callback called when getting the auth token completes. */ 260 @Test testAccountManagerCallbackRun()261 public void testAccountManagerCallbackRun() { 262 HttpNegotiateAuthenticator authenticator = createAuthenticator("Dummy_Account"); 263 264 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 265 266 // Call getNextAuthToken to get the callback 267 authenticator.getNextAuthToken(1234, "test_principal", "", true); 268 verify(sMockAccountManager) 269 .getAuthTokenByFeatures( 270 any(String.class), 271 any(String.class), 272 any(String[].class), 273 any(Activity.class), 274 (Bundle) isNull(), 275 any(Bundle.class), 276 mBundleCallbackCaptor.capture(), 277 any(Handler.class)); 278 279 Bundle resultBundle = new Bundle(); 280 Bundle context = new Bundle(); 281 context.putString("String", "test_context"); 282 resultBundle.putInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, HttpNegotiateConstants.OK); 283 resultBundle.putBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, context); 284 resultBundle.putString(AccountManager.KEY_AUTHTOKEN, "output_token"); 285 mBundleCallbackCaptor.getValue().run(makeFuture(resultBundle)); 286 verify(mAuthenticatorJniMock).setResult(1234, authenticator, 0, "output_token"); 287 288 // Check that the next call to getNextAuthToken uses the correct context 289 authenticator.getNextAuthToken(5678, "test_principal", "", true); 290 verify(sMockAccountManager, times(2)) 291 .getAuthTokenByFeatures( 292 any(String.class), 293 any(String.class), 294 any(String[].class), 295 any(Activity.class), 296 (Bundle) isNull(), 297 mBundleCaptor.capture(), 298 mBundleCallbackCaptor.capture(), 299 any(Handler.class)); 300 301 assertThat( 302 "The spnego context is preserved between calls", 303 mBundleCaptor.getValue().getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), 304 equalTo(context)); 305 306 // Test exception path 307 mBundleCallbackCaptor 308 .getValue() 309 .run(this.<Bundle>makeFuture(new OperationCanceledException())); 310 verify(mAuthenticatorJniMock).setResult(5678, authenticator, NetError.ERR_UNEXPECTED, null); 311 } 312 313 @Test testPermissionDenied()314 public void testPermissionDenied() { 315 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 316 HttpNegotiateAuthenticator authenticator = createAuthenticator("Dummy_Account", true); 317 318 authenticator.getNextAuthToken(1234, "test_principal", "", true); 319 verify(mAuthenticatorJniMock) 320 .setResult( 321 anyLong(), 322 eq(authenticator), 323 eq(NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT), 324 (String) isNull()); 325 } 326 327 @Test testAccountManagerCallbackNullErrorReturns()328 public void testAccountManagerCallbackNullErrorReturns() { 329 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 330 checkErrorReturn(null, NetError.ERR_UNEXPECTED); 331 } 332 333 @Test testAccountManagerCallbackUnexpectedErrorReturns()334 public void testAccountManagerCallbackUnexpectedErrorReturns() { 335 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 336 checkErrorReturn(HttpNegotiateConstants.ERR_UNEXPECTED, NetError.ERR_UNEXPECTED); 337 } 338 339 @Test testAccountManagerCallbackAbortedErrorReturns()340 public void testAccountManagerCallbackAbortedErrorReturns() { 341 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 342 checkErrorReturn(HttpNegotiateConstants.ERR_ABORTED, NetError.ERR_ABORTED); 343 } 344 345 @Test testAccountManagerCallbackSecLibErrorReturns()346 public void testAccountManagerCallbackSecLibErrorReturns() { 347 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 348 checkErrorReturn( 349 HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS, 350 NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS); 351 } 352 353 @Test testAccountManagerCallbackInvalidResponseErrorReturns()354 public void testAccountManagerCallbackInvalidResponseErrorReturns() { 355 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 356 checkErrorReturn( 357 HttpNegotiateConstants.ERR_INVALID_RESPONSE, NetError.ERR_INVALID_RESPONSE); 358 } 359 360 @Test testAccountManagerCallbackInvalidAuthCredsErrorReturns()361 public void testAccountManagerCallbackInvalidAuthCredsErrorReturns() { 362 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 363 checkErrorReturn( 364 HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS, 365 NetError.ERR_INVALID_AUTH_CREDENTIALS); 366 } 367 368 @Test testAccountManagerCallbackUnsuppAutchSchemeErrorReturns()369 public void testAccountManagerCallbackUnsuppAutchSchemeErrorReturns() { 370 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 371 checkErrorReturn( 372 HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME, 373 NetError.ERR_UNSUPPORTED_AUTH_SCHEME); 374 } 375 376 @Test testAccountManagerCallbackMissingAuthCredsErrorReturns()377 public void testAccountManagerCallbackMissingAuthCredsErrorReturns() { 378 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 379 checkErrorReturn( 380 HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS, 381 NetError.ERR_MISSING_AUTH_CREDENTIALS); 382 } 383 384 @Test testAccountManagerCallbackUndocSecLibErrorReturns()385 public void testAccountManagerCallbackUndocSecLibErrorReturns() { 386 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 387 checkErrorReturn( 388 HttpNegotiateConstants.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS, 389 NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS); 390 } 391 392 @Test testAccountManagerCallbackMalformedIdentityErrorReturns()393 public void testAccountManagerCallbackMalformedIdentityErrorReturns() { 394 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 395 checkErrorReturn( 396 HttpNegotiateConstants.ERR_MALFORMED_IDENTITY, NetError.ERR_MALFORMED_IDENTITY); 397 } 398 399 @Test testAccountManagerCallbackInvalidErrorReturns()400 public void testAccountManagerCallbackInvalidErrorReturns() { 401 Robolectric.buildActivity(Activity.class).create().start().resume().visible(); 402 // 9999 is not a valid return value 403 checkErrorReturn(9999, NetError.ERR_UNEXPECTED); 404 } 405 checkErrorReturn(Integer spnegoError, int expectedError)406 private void checkErrorReturn(Integer spnegoError, int expectedError) { 407 HttpNegotiateAuthenticator authenticator = createAuthenticator("Dummy_Account"); 408 409 // Call getNextAuthToken to get the callback 410 authenticator.getNextAuthToken(1234, "test_principal", "", true); 411 verify(sMockAccountManager) 412 .getAuthTokenByFeatures( 413 any(String.class), 414 any(String.class), 415 any(String[].class), 416 any(Activity.class), 417 (Bundle) isNull(), 418 any(Bundle.class), 419 mBundleCallbackCaptor.capture(), 420 any(Handler.class)); 421 422 Bundle resultBundle = new Bundle(); 423 if (spnegoError != null) { 424 resultBundle.putInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, spnegoError); 425 } 426 mBundleCallbackCaptor.getValue().run(makeFuture(resultBundle)); 427 verify(mAuthenticatorJniMock) 428 .setResult(anyLong(), eq(authenticator), eq(expectedError), (String) isNull()); 429 } 430 431 /** 432 * Returns a future that successfully returns the provided result. 433 * Hides mocking related annoyances: compiler warnings and irrelevant catch clauses. 434 */ makeFuture(T result)435 private <T> AccountManagerFuture<T> makeFuture(T result) { 436 // Avoid warning when creating mock accountManagerFuture, can't take .class of an 437 // instantiated generic type, yet compiler complains if I leave it uninstantiated. 438 @SuppressWarnings("unchecked") 439 AccountManagerFuture<T> accountManagerFuture = mock(AccountManagerFuture.class); 440 try { 441 when(accountManagerFuture.getResult()).thenReturn(result); 442 } catch (OperationCanceledException | AuthenticatorException | IOException e) { 443 // Can never happen - artifact of Mockito. 444 fail(); 445 } 446 return accountManagerFuture; 447 } 448 449 /** 450 * Returns a future that fails with the provided exception when trying to get its result. 451 * Hides mocking related annoyances: compiler warnings and irrelevant catch clauses. 452 */ makeFuture(Exception ex)453 private <T> AccountManagerFuture<T> makeFuture(Exception ex) { 454 // Avoid warning when creating mock accountManagerFuture, can't take .class of an 455 // instantiated generic type, yet compiler complains if I leave it uninstantiated. 456 @SuppressWarnings("unchecked") 457 AccountManagerFuture<T> accountManagerFuture = mock(AccountManagerFuture.class); 458 try { 459 when(accountManagerFuture.getResult()).thenThrow(ex); 460 } catch (OperationCanceledException | AuthenticatorException | IOException e) { 461 // Can never happen - artifact of Mockito. 462 fail(); 463 } 464 return accountManagerFuture; 465 } 466 467 /** Returns a new authenticator with an overridden lacksPermission method. */ createAuthenticator( String accountType, boolean lacksPermission)468 private HttpNegotiateAuthenticator createAuthenticator( 469 String accountType, boolean lacksPermission) { 470 return new HttpNegotiateAuthenticator(accountType) { 471 @Override 472 boolean lacksPermission(Context context, String permission, boolean onlyPreM) { 473 return lacksPermission; 474 } 475 }; 476 } 477 478 private HttpNegotiateAuthenticator createAuthenticator(String accountType) { 479 return createAuthenticator(accountType, false); 480 } 481 } 482