| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.net; |
| |
| import static org.hamcrest.CoreMatchers.equalTo; |
| import static org.hamcrest.CoreMatchers.notNullValue; |
| import static org.hamcrest.CoreMatchers.nullValue; |
| import static org.junit.Assert.assertThat; |
| import static org.junit.Assert.fail; |
| import static org.mockito.AdditionalMatchers.or; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyBoolean; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyLong; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.ArgumentMatchers.isNull; |
| import static org.mockito.Mockito.doNothing; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyZeroInteractions; |
| import static org.mockito.Mockito.when; |
| |
| import android.accounts.Account; |
| import android.accounts.AccountManager; |
| import android.accounts.AccountManagerCallback; |
| import android.accounts.AccountManagerFuture; |
| import android.accounts.AuthenticatorException; |
| import android.accounts.OperationCanceledException; |
| import android.app.Activity; |
| import android.app.Application; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.Bundle; |
| import android.os.Handler; |
| |
| import org.junit.After; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Captor; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| import org.robolectric.Robolectric; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.annotation.Implementation; |
| import org.robolectric.annotation.Implements; |
| import org.robolectric.shadows.ShadowAccountManager; |
| import org.robolectric.shadows.ShadowApplication; |
| import org.robolectric.shadows.multidex.ShadowMultiDex; |
| |
| import org.chromium.base.ApplicationStatus; |
| import org.chromium.base.test.BaseRobolectricTestRunner; |
| import org.chromium.net.HttpNegotiateAuthenticator.GetAccountsCallback; |
| import org.chromium.net.HttpNegotiateAuthenticator.RequestData; |
| |
| import java.io.IOException; |
| import java.util.List; |
| |
| /** |
| * Robolectric tests for HttpNegotiateAuthenticator |
| */ |
| @RunWith(BaseRobolectricTestRunner.class) |
| @Config(manifest = Config.NONE, |
| shadows = {HttpNegotiateAuthenticatorTest.ExtendedShadowAccountManager.class, |
| ShadowMultiDex.class}) |
| public class HttpNegotiateAuthenticatorTest { |
| /** |
| * User the AccountManager to inject a mock instance. |
| * Note: Shadow classes need to be public and static. |
| */ |
| @Implements(AccountManager.class) |
| public static class ExtendedShadowAccountManager extends ShadowAccountManager { |
| @Implementation |
| public static AccountManager get(Context context) { |
| return sMockAccountManager; |
| } |
| } |
| |
| @Mock |
| private static AccountManager sMockAccountManager; |
| @Captor |
| private ArgumentCaptor<AccountManagerCallback<Bundle>> mBundleCallbackCaptor; |
| @Captor |
| private ArgumentCaptor<AccountManagerCallback<Account[]>> mAccountCallbackCaptor; |
| @Captor |
| private ArgumentCaptor<Bundle> mBundleCaptor; |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| ApplicationStatus.initialize(RuntimeEnvironment.application); |
| } |
| |
| @After |
| public void tearDown() { |
| ApplicationStatus.destroyForJUnitTests(); |
| } |
| |
| /** |
| * Test of {@link HttpNegotiateAuthenticator#getNextAuthToken} |
| */ |
| @Test |
| public void testGetNextAuthToken() { |
| final String accountType = "Dummy_Account"; |
| HttpNegotiateAuthenticator authenticator = createWithoutNative(accountType); |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| |
| authenticator.getNextAuthToken(0, "test_principal", "", true); |
| |
| verify(sMockAccountManager) |
| .getAuthTokenByFeatures(eq(accountType), eq("SPNEGO:HOSTBASED:test_principal"), |
| eq(new String[] {"SPNEGO"}), any(Activity.class), (Bundle) isNull(), |
| mBundleCaptor.capture(), mBundleCallbackCaptor.capture(), |
| any(Handler.class)); |
| |
| assertThat("There is no existing context", |
| mBundleCaptor.getValue().get(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), |
| nullValue()); |
| assertThat("The existing token is empty", |
| mBundleCaptor.getValue().getString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN), |
| equalTo("")); |
| assertThat("Delegation is allowed", |
| mBundleCaptor.getValue().getBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE), |
| equalTo(true)); |
| assertThat("getAuthTokenByFeatures was called with a callback", |
| mBundleCallbackCaptor.getValue(), notNullValue()); |
| } |
| |
| /** |
| * Test of {@link HttpNegotiateAuthenticator#getNextAuthToken} without a visible activity. |
| * This emulates the behavior with WebView, where the application is a generic one and doesn't |
| * set up the ApplicationStatus the same way. |
| */ |
| @Test |
| @Config(application = Application.class) |
| public void testGetNextAuthTokenWithoutActivity() { |
| final String accountType = "Dummy_Account"; |
| final Account[] returnedAccount = {new Account("name", accountType)}; |
| HttpNegotiateAuthenticator authenticator = createWithoutNative(accountType); |
| |
| authenticator.getNextAuthToken(1234, "test_principal", "", true); |
| |
| Assert.assertNull(ApplicationStatus.getLastTrackedFocusedActivity()); |
| verify(sMockAccountManager).getAccountsByTypeAndFeatures( |
| eq(accountType), |
| eq(new String[]{"SPNEGO"}), |
| mAccountCallbackCaptor.capture(), |
| any(Handler.class)); |
| |
| mAccountCallbackCaptor.getValue().run(makeFuture(returnedAccount)); |
| |
| verify(sMockAccountManager).getAuthToken( |
| any(Account.class), |
| eq("SPNEGO:HOSTBASED:test_principal"), |
| mBundleCaptor.capture(), |
| eq(true), |
| any(HttpNegotiateAuthenticator.GetTokenCallback.class), |
| any(Handler.class)); |
| |
| assertThat("There is no existing context", |
| mBundleCaptor.getValue().get(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), |
| nullValue()); |
| assertThat("The existing token is empty", |
| mBundleCaptor.getValue().getString(HttpNegotiateConstants.KEY_INCOMING_AUTH_TOKEN), |
| equalTo("")); |
| assertThat("Delegation is allowed", |
| mBundleCaptor.getValue().getBoolean(HttpNegotiateConstants.KEY_CAN_DELEGATE), |
| equalTo(true)); |
| } |
| |
| /** Tests the behavior of {@link HttpNegotiateAuthenticator.GetAccountsCallback} */ |
| @Test |
| public void testGetAccountCallback() { |
| String type = "Dummy_Account"; |
| HttpNegotiateAuthenticator authenticator = createWithoutNative(type); |
| RequestData requestData = new RequestData(); |
| requestData.nativeResultObject = 42; |
| requestData.accountManager = sMockAccountManager; |
| GetAccountsCallback callback = authenticator.new GetAccountsCallback(requestData); |
| |
| // Should fail because there are no accounts |
| callback.run(makeFuture(new Account[]{})); |
| verify(authenticator) |
| .nativeSetResult( |
| eq(42L), eq(NetError.ERR_MISSING_AUTH_CREDENTIALS), (String) isNull()); |
| |
| // Should succeed, for a single account we use it for the AccountManager#getAuthToken call. |
| Account testAccount = new Account("a", type); |
| callback.run(makeFuture(new Account[]{testAccount})); |
| verify(sMockAccountManager) |
| .getAuthToken(eq(testAccount), (String) isNull(), (Bundle) isNull(), eq(true), |
| any(HttpNegotiateAuthenticator.GetTokenCallback.class), any(Handler.class)); |
| |
| // Should fail because there is more than one account |
| callback.run(makeFuture(new Account[]{new Account("a", type), new Account("b", type)})); |
| verify(authenticator, times(2)) |
| .nativeSetResult( |
| eq(42L), eq(NetError.ERR_MISSING_AUTH_CREDENTIALS), (String) isNull()); |
| } |
| |
| /** |
| * Tests the behavior of {@link HttpNegotiateAuthenticator.GetTokenCallback} when the result it |
| * receives contains an intent rather than a token directly. |
| */ |
| @Test |
| public void testGetTokenCallbackWithIntent() { |
| String type = "Dummy_Account"; |
| HttpNegotiateAuthenticator authenticator = createWithoutNative(type); |
| RequestData requestData = new RequestData(); |
| requestData.nativeResultObject = 42; |
| requestData.authTokenType = "foo"; |
| requestData.account = new Account("a", type); |
| requestData.accountManager = sMockAccountManager; |
| Bundle b = new Bundle(); |
| b.putParcelable(AccountManager.KEY_INTENT, new Intent()); |
| |
| authenticator.new GetTokenCallback(requestData).run(makeFuture(b)); |
| verifyZeroInteractions(sMockAccountManager); |
| |
| // Verify that the broadcast receiver is registered |
| Intent intent = new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION); |
| ShadowApplication shadowApplication = ShadowApplication.getInstance(); |
| List<BroadcastReceiver> receivers = shadowApplication.getReceiversForIntent(intent); |
| assertThat("There is one registered broadcast receiver", receivers.size(), equalTo(1)); |
| |
| // Send the intent to the receiver. |
| BroadcastReceiver receiver = receivers.get(0); |
| receiver.onReceive(ShadowApplication.getInstance().getApplicationContext(), intent); |
| |
| // Verify that the auth token is properly requested from the account manager. |
| verify(sMockAccountManager) |
| .getAuthToken(eq(new Account("a", type)), eq("foo"), (Bundle) isNull(), eq(true), |
| any(HttpNegotiateAuthenticator.GetTokenCallback.class), (Handler) isNull()); |
| } |
| |
| /** |
| * Test of callback called when getting the auth token completes. |
| */ |
| @Test |
| public void testAccountManagerCallbackRun() { |
| HttpNegotiateAuthenticator authenticator = createWithoutNative("Dummy_Account"); |
| |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| |
| // Call getNextAuthToken to get the callback |
| authenticator.getNextAuthToken(1234, "test_principal", "", true); |
| verify(sMockAccountManager) |
| .getAuthTokenByFeatures(any(String.class), any(String.class), any(String[].class), |
| any(Activity.class), (Bundle) isNull(), any(Bundle.class), |
| mBundleCallbackCaptor.capture(), any(Handler.class)); |
| |
| Bundle resultBundle = new Bundle(); |
| Bundle context = new Bundle(); |
| context.putString("String", "test_context"); |
| resultBundle.putInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, HttpNegotiateConstants.OK); |
| resultBundle.putBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT, context); |
| resultBundle.putString(AccountManager.KEY_AUTHTOKEN, "output_token"); |
| mBundleCallbackCaptor.getValue().run(makeFuture(resultBundle)); |
| verify(authenticator).nativeSetResult(1234, 0, "output_token"); |
| |
| // Check that the next call to getNextAuthToken uses the correct context |
| authenticator.getNextAuthToken(5678, "test_principal", "", true); |
| verify(sMockAccountManager, times(2)) |
| .getAuthTokenByFeatures(any(String.class), any(String.class), any(String[].class), |
| any(Activity.class), (Bundle) isNull(), mBundleCaptor.capture(), |
| mBundleCallbackCaptor.capture(), any(Handler.class)); |
| |
| assertThat("The spnego context is preserved between calls", |
| mBundleCaptor.getValue().getBundle(HttpNegotiateConstants.KEY_SPNEGO_CONTEXT), |
| equalTo(context)); |
| |
| // Test exception path |
| mBundleCallbackCaptor.getValue().run( |
| this.<Bundle>makeFuture(new OperationCanceledException())); |
| verify(authenticator).nativeSetResult(5678, NetError.ERR_UNEXPECTED, null); |
| } |
| |
| @Test |
| public void testPermissionDenied() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| HttpNegotiateAuthenticator authenticator = createWithoutNative("Dummy_Account"); |
| |
| doReturn(true) |
| .when(authenticator) |
| .lacksPermission(any(Context.class), any(String.class), anyBoolean()); |
| |
| authenticator.getNextAuthToken(1234, "test_principal", "", true); |
| verify(authenticator) |
| .nativeSetResult(anyLong(), eq(NetError.ERR_MISCONFIGURED_AUTH_ENVIRONMENT), |
| (String) isNull()); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackNullErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(null, NetError.ERR_UNEXPECTED); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackUnexpectedErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(HttpNegotiateConstants.ERR_UNEXPECTED, NetError.ERR_UNEXPECTED); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackAbortedErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(HttpNegotiateConstants.ERR_ABORTED, NetError.ERR_ABORTED); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackSecLibErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(HttpNegotiateConstants.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS, |
| NetError.ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackInvalidResponseErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn( |
| HttpNegotiateConstants.ERR_INVALID_RESPONSE, NetError.ERR_INVALID_RESPONSE); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackInvalidAuthCredsErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(HttpNegotiateConstants.ERR_INVALID_AUTH_CREDENTIALS, |
| NetError.ERR_INVALID_AUTH_CREDENTIALS); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackUnsuppAutchSchemeErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(HttpNegotiateConstants.ERR_UNSUPPORTED_AUTH_SCHEME, |
| NetError.ERR_UNSUPPORTED_AUTH_SCHEME); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackMissingAuthCredsErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(HttpNegotiateConstants.ERR_MISSING_AUTH_CREDENTIALS, |
| NetError.ERR_MISSING_AUTH_CREDENTIALS); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackUndocSecLibErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn(HttpNegotiateConstants.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS, |
| NetError.ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackMalformedIdentityErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| checkErrorReturn( |
| HttpNegotiateConstants.ERR_MALFORMED_IDENTITY, NetError.ERR_MALFORMED_IDENTITY); |
| } |
| |
| @Test |
| public void testAccountManagerCallbackInvalidErrorReturns() { |
| Robolectric.buildActivity(Activity.class).create().start().resume().visible(); |
| // 9999 is not a valid return value |
| checkErrorReturn(9999, NetError.ERR_UNEXPECTED); |
| } |
| |
| private void checkErrorReturn(Integer spnegoError, int expectedError) { |
| HttpNegotiateAuthenticator authenticator = createWithoutNative("Dummy_Account"); |
| |
| // Call getNextAuthToken to get the callback |
| authenticator.getNextAuthToken(1234, "test_principal", "", true); |
| verify(sMockAccountManager) |
| .getAuthTokenByFeatures(any(String.class), any(String.class), any(String[].class), |
| any(Activity.class), (Bundle) isNull(), any(Bundle.class), |
| mBundleCallbackCaptor.capture(), any(Handler.class)); |
| |
| Bundle resultBundle = new Bundle(); |
| if (spnegoError != null) { |
| resultBundle.putInt(HttpNegotiateConstants.KEY_SPNEGO_RESULT, spnegoError); |
| } |
| mBundleCallbackCaptor.getValue().run(makeFuture(resultBundle)); |
| verify(authenticator).nativeSetResult(anyLong(), eq(expectedError), (String) isNull()); |
| } |
| |
| /** |
| * Returns a future that successfully returns the provided result. |
| * Hides mocking related annoyances: compiler warnings and irrelevant catch clauses. |
| */ |
| private <T> AccountManagerFuture<T> makeFuture(T result) { |
| // Avoid warning when creating mock accountManagerFuture, can't take .class of an |
| // instantiated generic type, yet compiler complains if I leave it uninstantiated. |
| @SuppressWarnings("unchecked") |
| AccountManagerFuture<T> accountManagerFuture = mock(AccountManagerFuture.class); |
| try { |
| when(accountManagerFuture.getResult()).thenReturn(result); |
| } catch (OperationCanceledException | AuthenticatorException | IOException e) { |
| // Can never happen - artifact of Mockito. |
| fail(); |
| } |
| return accountManagerFuture; |
| } |
| |
| /** |
| * Returns a future that fails with the provided exception when trying to get its result. |
| * Hides mocking related annoyances: compiler warnings and irrelevant catch clauses. |
| */ |
| private <T> AccountManagerFuture<T> makeFuture(Exception ex) { |
| // Avoid warning when creating mock accountManagerFuture, can't take .class of an |
| // instantiated generic type, yet compiler complains if I leave it uninstantiated. |
| @SuppressWarnings("unchecked") |
| AccountManagerFuture<T> accountManagerFuture = mock(AccountManagerFuture.class); |
| try { |
| when(accountManagerFuture.getResult()).thenThrow(ex); |
| } catch (OperationCanceledException | AuthenticatorException | IOException e) { |
| // Can never happen - artifact of Mockito. |
| fail(); |
| } |
| return accountManagerFuture; |
| } |
| |
| /** |
| * Returns a new authenticator as a spy so that we can override and intercept the native method |
| * calls. |
| */ |
| private HttpNegotiateAuthenticator createWithoutNative(String accountType) { |
| HttpNegotiateAuthenticator authenticator = |
| spy(HttpNegotiateAuthenticator.create(accountType)); |
| doNothing() |
| .when(authenticator) |
| .nativeSetResult(anyLong(), anyInt(), or(any(String.class), (String) isNull())); |
| doReturn(false) |
| .when(authenticator) |
| .lacksPermission(any(Context.class), any(String.class), anyBoolean()); |
| return authenticator; |
| } |
| } |