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