blob: 9d381cf86d874a19eccc822053781b0cf436411c [file] [log] [blame]
// 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;
}
}