// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.safety_check;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.IntentMatchers.anyIntent;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.chromium.chrome.browser.password_manager.PasswordCheckReferrer.SAFETY_CHECK;
import static org.chromium.chrome.browser.safety_check.PasswordsCheckPreferenceProperties.COMPROMISED_PASSWORDS_COUNT;
import static org.chromium.chrome.browser.safety_check.PasswordsCheckPreferenceProperties.PASSWORDS_STATE;
import static org.chromium.chrome.browser.safety_check.SafetyCheckProperties.SAFE_BROWSING_STATE;
import static org.chromium.chrome.browser.safety_check.SafetyCheckProperties.UPDATES_STATE;
import android.app.Activity;
import android.app.Instrumentation.ActivityResult;
import android.content.Intent;
import android.os.Handler;
import androidx.preference.Preference;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.espresso.intent.Intents;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
import org.robolectric.annotation.Config;
import org.chromium.base.Callback;
import org.chromium.base.CollectionUtil;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.shared_preferences.SharedPreferencesManager;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.test.BaseRobolectricTestRule;
import org.chromium.base.test.util.Features;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.loading_modal.LoadingModalDialogCoordinator;
import org.chromium.chrome.browser.password_check.PasswordCheck;
import org.chromium.chrome.browser.password_check.PasswordCheckFactory;
import org.chromium.chrome.browser.password_check.PasswordCheckUIStatus;
import org.chromium.chrome.browser.password_manager.CredentialManagerLauncher;
import org.chromium.chrome.browser.password_manager.CredentialManagerLauncher.CredentialManagerBackendException;
import org.chromium.chrome.browser.password_manager.CredentialManagerLauncher.CredentialManagerError;
import org.chromium.chrome.browser.password_manager.CredentialManagerLauncherFactory;
import org.chromium.chrome.browser.password_manager.ManagePasswordsReferrer;
import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelper;
import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelper.PasswordCheckBackendException;
import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelperFactory;
import org.chromium.chrome.browser.password_manager.PasswordManagerBackendSupportHelper;
import org.chromium.chrome.browser.password_manager.PasswordManagerHelper;
import org.chromium.chrome.browser.password_manager.PasswordManagerHelperJni;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridge;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridgeJni;
import org.chromium.chrome.browser.password_manager.PasswordStoreBridge;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.pwd_check_wrapper.FakePasswordCheckControllerFactory;
import org.chromium.chrome.browser.pwd_check_wrapper.PasswordCheckController.PasswordCheckResult;
import org.chromium.chrome.browser.pwd_check_wrapper.PasswordCheckController.PasswordStorageType;
import org.chromium.chrome.browser.pwd_check_wrapper.PasswordCheckNativeException;
import org.chromium.chrome.browser.safety_check.PasswordsCheckPreferenceProperties.PasswordsState;
import org.chromium.chrome.browser.safety_check.SafetyCheckMediator.SafetyCheckInteractions;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.SafeBrowsingState;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.UpdatesState;
import org.chromium.chrome.browser.settings.SettingsLauncherFactory;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.ui.signin.SigninAndHistorySyncActivityLauncher;
import org.chromium.chrome.browser.ui.signin.SigninAndHistorySyncCoordinator;
import org.chromium.chrome.browser.ui.signin.SyncConsentActivityLauncher;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.metrics.SigninAccessPoint;
import org.chromium.components.sync.SyncService;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.components.user_prefs.UserPrefsJni;
import org.chromium.content_public.browser.BrowserContextHandle;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
/** Unit tests for {@link SafetyCheckMediator}. */
@RunWith(ParameterizedRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@DisableFeatures(
ChromeFeatureList.UNIFIED_PASSWORD_MANAGER_LOCAL_PASSWORDS_ANDROID_ACCESS_LOSS_WARNING)
public class SafetyCheckMediatorTest {
private static final String SAFETY_CHECK_INTERACTIONS_HISTOGRAM =
"Settings.SafetyCheck.Interactions";
private static final String SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM =
"Settings.SafetyCheck.PasswordsResult2";
private static final String SAFETY_CHECK_SAFE_BROWSING_RESULT_HISTOGRAM =
"Settings.SafetyCheck.SafeBrowsingResult";
private static final String SAFETY_CHECK_UPDATES_RESULT_HISTOGRAM =
"Settings.SafetyCheck.UpdatesResult";
private static final String TEST_EMAIL_ADDRESS = "[email protected]";
@Rule(order = -2)
public BaseRobolectricTestRule mBaseRule = new BaseRobolectricTestRule();
@Rule public JniMocker mJniMocker = new JniMocker();
private PropertyModel mSafetyCheckModel;
private PropertyModel mPasswordCheckModel;
@Mock private SafetyCheckBridge.Natives mSafetyCheckBridge;
@Mock private Profile mProfile;
@Mock private SafetyCheckUpdatesDelegate mUpdatesDelegate;
@Mock private SigninAndHistorySyncActivityLauncher mSigninLauncher;
@Mock private SyncConsentActivityLauncher mSyncLauncher;
@Mock private SettingsLauncher mSettingsLauncher;
@Mock private SyncService mSyncService;
@Mock private Handler mHandler;
@Mock private PasswordCheck mPasswordCheck;
// TODO(crbug.com/40854050): Use existing fake instead of mocking
@Mock private PasswordCheckupClientHelper mPasswordCheckupHelper;
@Mock private CredentialManagerLauncher mCredentialManagerLauncher;
@Mock private PasswordStoreBridge mPasswordStoreBridge;
@Mock private PrefService mPrefService;
@Mock private UserPrefs.Natives mUserPrefsJniMock;
// TODO(crbug.com/40854050): Use fake instead of mocking
@Mock private PasswordManagerBackendSupportHelper mBackendSupportHelperMock;
@Mock private PasswordManagerUtilBridge.Natives mPasswordManagerUtilBridgeNativeMock;
@Mock private PasswordManagerHelper.Natives mPasswordManagerHelperNativeMock;
@Mock LoadingModalDialogCoordinator mLoadingModalDialogCoordinator;
private FakePasswordCheckControllerFactory mPasswordCheckControllerFactory;
private SafetyCheckMediator mMediator;
private boolean mUseGmsApi;
private ModalDialogManager mModalDialogManager;
private final ObservableSupplierImpl<ModalDialogManager> mModalDialogManagerSupplier =
new ObservableSupplierImpl<>();
private LoadingModalDialogCoordinator.Observer mLoadingDialogCoordinatorObserver;
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {{false}, {true}});
}
public SafetyCheckMediatorTest(boolean useGmsApi) {
mUseGmsApi = useGmsApi;
ContextUtils.initApplicationContextForTests(ApplicationProvider.getApplicationContext());
}
private void setUpPasswordCheckToReturnError(
@PasswordStorageType int passwordStorageType, Exception error) {
mPasswordCheckControllerFactory
.getLastCreatedController()
.setPasswordCheckResult(passwordStorageType, new PasswordCheckResult(error));
}
private void setUpPasswordCheckToReturnNoPasswords(
@PasswordStorageType int passwordStorageType) {
if (mUseGmsApi) {
mPasswordCheckControllerFactory
.getLastCreatedController()
.setPasswordCheckResult(
passwordStorageType,
new PasswordCheckResult(
/* totalPasswordsCount= */ 0, /* breachedCount= */ 00));
} else {
PasswordCheckNativeException noPasswordsError =
new PasswordCheckNativeException(
"Test exception", PasswordCheckUIStatus.ERROR_NO_PASSWORDS);
mPasswordCheckControllerFactory
.getLastCreatedController()
.setPasswordCheckResult(
passwordStorageType, new PasswordCheckResult(noPasswordsError));
}
}
private void setUpPasswordCheckToReturnResult(
@PasswordStorageType int passwordStorageType, PasswordCheckResult result) {
mPasswordCheckControllerFactory
.getLastCreatedController()
.setPasswordCheckResult(passwordStorageType, result);
}
private void configureMockSyncService() {
// SyncService is injected in the mediator, but dependencies still access the factory.
SyncServiceFactory.setInstanceForTesting(mSyncService);
when(mSyncService.isSyncFeatureEnabled()).thenReturn(true);
when(mSyncService.isEngineInitialized()).thenReturn(true);
when(mSyncService.hasSyncConsent()).thenReturn(true);
when(mSyncService.getAccountInfo())
.thenReturn(CoreAccountInfo.createFromEmailAndGaiaId(TEST_EMAIL_ADDRESS, "0"));
when(mPasswordManagerHelperNativeMock.hasChosenToSyncPasswords(mSyncService))
.thenReturn(true);
// TODO(crbug.com/41483841): Parametrize the tests in SafetyCheckMediatorTest for local and
// account storage.
// This will no longer be true once the local and account store split happens.
if (mUseGmsApi) {
when(mSyncService.getSelectedTypes())
.thenReturn(CollectionUtil.newHashSet(UserSelectableType.PASSWORDS));
} else {
when(mSyncService.getSelectedTypes()).thenReturn(new HashSet<>());
}
}
private SafetyCheckMediator createSafetyCheckMediator(
PropertyModel passwordCheckAccountModel, PropertyModel passwordCheckLocalModel) {
return new SafetyCheckMediator(
mProfile,
mSafetyCheckModel,
passwordCheckAccountModel,
passwordCheckLocalModel,
mUpdatesDelegate,
new SafetyCheckBridge(mProfile),
mSigninLauncher,
mSyncLauncher,
mSyncService,
mPrefService,
mPasswordStoreBridge,
mPasswordCheckControllerFactory,
PasswordManagerHelper.getForProfile(mProfile),
mHandler,
mModalDialogManagerSupplier);
}
private Preference.OnPreferenceClickListener getPasswordsClickListener(
PropertyModel passwordCheckModel) {
return (Preference.OnPreferenceClickListener)
passwordCheckModel.get(PasswordsCheckPreferenceProperties.PASSWORDS_CLICK_LISTENER);
}
private void click(Preference.OnPreferenceClickListener listener) {
listener.onPreferenceClick(new Preference(ContextUtils.getApplicationContext()));
}
@Before
public void setUp() throws PasswordCheckBackendException, CredentialManagerBackendException {
MockitoAnnotations.initMocks(this);
mJniMocker.mock(
PasswordManagerUtilBridgeJni.TEST_HOOKS, mPasswordManagerUtilBridgeNativeMock);
mJniMocker.mock(PasswordManagerHelperJni.TEST_HOOKS, mPasswordManagerHelperNativeMock);
when(mProfile.getOriginalProfile()).thenReturn(mProfile);
configureMockSyncService();
SettingsLauncherFactory.setInstanceForTesting(mSettingsLauncher);
PasswordManagerBackendSupportHelper.setInstanceForTesting(mBackendSupportHelperMock);
when(mBackendSupportHelperMock.isBackendPresent()).thenReturn(true);
when(mPasswordManagerUtilBridgeNativeMock.areMinUpmRequirementsMet()).thenReturn(true);
// Availability of the UPM backend will be checked by the SafetyCheckMediator using
// PasswordManagerHelper so the bridge method needs to be mocked.
// The parameter mUseGmsApi currently means that the mock SyncService will be configured to
// sync passwords, which so far is the only case in which the GMS APIs can be used.
when(mPasswordManagerUtilBridgeNativeMock.shouldUseUpmWiring(mSyncService, mPrefService))
.thenReturn(mUseGmsApi);
mJniMocker.mock(SafetyCheckBridgeJni.TEST_HOOKS, mSafetyCheckBridge);
mJniMocker.mock(UserPrefsJni.TEST_HOOKS, mUserPrefsJniMock);
when(mUserPrefsJniMock.get(mProfile)).thenReturn(mPrefService);
when(mPrefService.getBoolean(Pref.UNENROLLED_FROM_GOOGLE_MOBILE_SERVICES_DUE_TO_ERRORS))
.thenReturn(false);
mSafetyCheckModel = SafetyCheckProperties.createSafetyCheckModel();
mPasswordCheckModel =
PasswordsCheckPreferenceProperties.createPasswordSafetyCheckModel("Passwords");
mPasswordCheckControllerFactory = new FakePasswordCheckControllerFactory();
if (mUseGmsApi) {
// TODO(crbug.com/40854050): Use existing fake instead of mocking
PasswordCheckupClientHelperFactory mockPasswordCheckFactory =
mock(PasswordCheckupClientHelperFactory.class);
when(mockPasswordCheckFactory.createHelper()).thenReturn(mPasswordCheckupHelper);
PasswordCheckupClientHelperFactory.setFactoryForTesting(mockPasswordCheckFactory);
CredentialManagerLauncherFactory mockCredentialManagerLauncherFactory =
mock(CredentialManagerLauncherFactory.class);
when(mockCredentialManagerLauncherFactory.createLauncher())
.thenReturn(mCredentialManagerLauncher);
CredentialManagerLauncherFactory.setFactoryForTesting(
mockCredentialManagerLauncherFactory);
} else {
PasswordCheckFactory.setPasswordCheckForTesting(mPasswordCheck);
}
mMediator =
createSafetyCheckMediator(mPasswordCheckModel, /* passwordCheckLocalModel= */ null);
// Execute any delayed tasks immediately.
doAnswer(
invocation -> {
Runnable runnable = (Runnable) (invocation.getArguments()[0]);
runnable.run();
return null;
})
.when(mHandler)
.postDelayed(any(Runnable.class), anyLong());
// User is always signed in unless the test specifies otherwise.
doReturn(true).when(mSafetyCheckBridge).userSignedIn(any(BrowserContextHandle.class));
// Reset the histogram count.
mModalDialogManager =
new ModalDialogManager(
mock(ModalDialogManager.Presenter.class),
ModalDialogManager.ModalDialogType.APP);
mModalDialogManagerSupplier.set(mModalDialogManager);
doAnswer(
invocation -> {
mLoadingDialogCoordinatorObserver = invocation.getArgument(0);
return null;
})
.when(mLoadingModalDialogCoordinator)
.addObserver(any(LoadingModalDialogCoordinator.Observer.class));
}
@Test
public void testStartInteractionRecorded() {
mMediator.performSafetyCheck();
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_INTERACTIONS_HISTOGRAM, SafetyCheckInteractions.STARTED));
}
@Test
public void testUpdatesCheckUpdated() {
doAnswer(
invocation -> {
Callback<Integer> callback =
((WeakReference<Callback<Integer>>)
invocation.getArguments()[0])
.get();
callback.onResult(UpdatesState.UPDATED);
return null;
})
.when(mUpdatesDelegate)
.checkForUpdates(any(WeakReference.class));
mMediator.performSafetyCheck();
assertEquals(UpdatesState.UPDATED, mSafetyCheckModel.get(UPDATES_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_UPDATES_RESULT_HISTOGRAM, UpdateStatus.UPDATED));
}
@Test
public void testUpdatesCheckOutdated() {
doAnswer(
invocation -> {
Callback<Integer> callback =
((WeakReference<Callback<Integer>>)
invocation.getArguments()[0])
.get();
callback.onResult(UpdatesState.OUTDATED);
return null;
})
.when(mUpdatesDelegate)
.checkForUpdates(any(WeakReference.class));
mMediator.performSafetyCheck();
assertEquals(UpdatesState.OUTDATED, mSafetyCheckModel.get(UPDATES_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_UPDATES_RESULT_HISTOGRAM, UpdateStatus.OUTDATED));
}
@Test
public void testSafeBrowsingCheckEnabledStandard() {
doReturn(SafeBrowsingStatus.ENABLED_STANDARD)
.when(mSafetyCheckBridge)
.checkSafeBrowsing(any(BrowserContextHandle.class));
mMediator.performSafetyCheck();
assertEquals(
SafeBrowsingState.ENABLED_STANDARD, mSafetyCheckModel.get(SAFE_BROWSING_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_SAFE_BROWSING_RESULT_HISTOGRAM,
SafeBrowsingStatus.ENABLED_STANDARD));
}
@Test
public void testSafeBrowsingCheckDisabled() {
doReturn(SafeBrowsingStatus.DISABLED)
.when(mSafetyCheckBridge)
.checkSafeBrowsing(any(BrowserContextHandle.class));
mMediator.performSafetyCheck();
assertEquals(SafeBrowsingState.DISABLED, mSafetyCheckModel.get(SAFE_BROWSING_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_SAFE_BROWSING_RESULT_HISTOGRAM, SafeBrowsingStatus.DISABLED));
}
@Test
public void testPasswordsCheckError() {
mMediator.performSafetyCheck();
setUpPasswordCheckToReturnError(
PasswordStorageType.ACCOUNT_STORAGE, new Exception("Test exception"));
assertEquals(PasswordsState.ERROR, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.ERROR));
}
@Test
public void testPasswordsCheckBackendOutdated() {
if (!mUseGmsApi) return;
mMediator.performSafetyCheck();
setUpPasswordCheckToReturnError(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckBackendException(
"test", CredentialManagerError.BACKEND_VERSION_NOT_SUPPORTED));
assertEquals(
PasswordsState.BACKEND_VERSION_NOT_SUPPORTED,
mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.ERROR));
}
@Test
public void testPasswordsCheckNoPasswords() {
mMediator.performSafetyCheck();
setUpPasswordCheckToReturnNoPasswords(PasswordStorageType.ACCOUNT_STORAGE);
assertEquals(PasswordsState.NO_PASSWORDS, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.NO_PASSWORDS));
}
@Test
public void testPasswordsCheckNoLeaks() {
mMediator.performSafetyCheck();
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 20, /* breachedCount= */ 0));
assertEquals(PasswordsState.SAFE, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.SAFE));
}
@Test
public void testPasswordsCheckHasLeaks() {
final int numLeaks = 123;
mMediator.performSafetyCheck();
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 199, numLeaks));
assertEquals(PasswordsState.COMPROMISED_EXIST, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(numLeaks, mPasswordCheckModel.get(COMPROMISED_PASSWORDS_COUNT));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM,
PasswordsStatus.COMPROMISED_EXIST));
}
@Test
public void testNullStateLessThan10MinsPasswordsSafeState() {
// Ran just now.
SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
preferenceManager.writeLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis());
// Safe Browsing: on.
doReturn(SafeBrowsingStatus.ENABLED_STANDARD)
.when(mSafetyCheckBridge)
.checkSafeBrowsing(any(BrowserContextHandle.class));
// Updates: outdated.
doAnswer(
invocation -> {
Callback<Integer> callback =
((WeakReference<Callback<Integer>>)
invocation.getArguments()[0])
.get();
callback.onResult(UpdatesState.OUTDATED);
return null;
})
.when(mUpdatesDelegate)
.checkForUpdates(any(WeakReference.class));
mMediator.setInitialState();
// Passwords: safe state.
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 12, /* breachedCount= */ 0));
// Verify the states.
assertEquals(
SafeBrowsingState.ENABLED_STANDARD, mSafetyCheckModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.SAFE, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.OUTDATED, mSafetyCheckModel.get(UPDATES_STATE));
}
@Test
public void testNullStateLessThan10MinsNoSavedPasswords() {
// Ran just now.
SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
preferenceManager.writeLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis());
// Safe Browsing: disabled by admin.
doReturn(SafeBrowsingStatus.DISABLED_BY_ADMIN)
.when(mSafetyCheckBridge)
.checkSafeBrowsing(any(BrowserContextHandle.class));
// Updates: offline.
doAnswer(
invocation -> {
Callback<Integer> callback =
((WeakReference<Callback<Integer>>)
invocation.getArguments()[0])
.get();
callback.onResult(UpdatesState.OFFLINE);
return null;
})
.when(mUpdatesDelegate)
.checkForUpdates(any(WeakReference.class));
mMediator.setInitialState();
// Passwords: no passwords.
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 0, /* breachedCount= */ 0));
// Verify the states.
assertEquals(
SafeBrowsingState.DISABLED_BY_ADMIN, mSafetyCheckModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.NO_PASSWORDS, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.OFFLINE, mSafetyCheckModel.get(UPDATES_STATE));
}
@Test
public void testNullStateLessThan10MinsPasswordsUnsafeState() {
// Ran just now.
SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
preferenceManager.writeLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis());
// Safe Browsing: off.
doReturn(SafeBrowsingStatus.DISABLED)
.when(mSafetyCheckBridge)
.checkSafeBrowsing(any(BrowserContextHandle.class));
// Updates: updated.
doAnswer(
invocation -> {
Callback<Integer> callback =
((WeakReference<Callback<Integer>>)
invocation.getArguments()[0])
.get();
callback.onResult(UpdatesState.UPDATED);
return null;
})
.when(mUpdatesDelegate)
.checkForUpdates(any(WeakReference.class));
mMediator.setInitialState();
// Passwords: compromised state.
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 20, /* breachedCount= */ 18));
// Verify the states.
assertEquals(SafeBrowsingState.DISABLED, mSafetyCheckModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.COMPROMISED_EXIST, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.UPDATED, mSafetyCheckModel.get(UPDATES_STATE));
}
@Test
public void testNullStateMoreThan10MinsPasswordsSafeState() {
// Ran 20 mins ago.
SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
preferenceManager.writeLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis() - (20 * 60 * 1000));
// Safe Browsing: on.
doReturn(SafeBrowsingStatus.ENABLED_STANDARD)
.when(mSafetyCheckBridge)
.checkSafeBrowsing(any(BrowserContextHandle.class));
// Updates: outdated.
doAnswer(
invocation -> {
Callback<Integer> callback =
((WeakReference<Callback<Integer>>)
invocation.getArguments()[0])
.get();
callback.onResult(UpdatesState.OUTDATED);
return null;
})
.when(mUpdatesDelegate)
.checkForUpdates(any(WeakReference.class));
mMediator.setInitialState();
// Passwords: safe state.
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 13, /* breachedCount= */ 0));
// Verify the states.
assertEquals(SafeBrowsingState.UNCHECKED, mSafetyCheckModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.UNCHECKED, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.UNCHECKED, mSafetyCheckModel.get(UPDATES_STATE));
}
@Test
public void testNullStateMoreThan10MinsPasswordsUnsafeState() {
// Ran 20 mins ago.
SharedPreferencesManager preferenceManager = ChromeSharedPreferences.getInstance();
preferenceManager.writeLong(
ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis() - (20 * 60 * 1000));
// Safe Browsing: off.
doReturn(SafeBrowsingStatus.DISABLED)
.when(mSafetyCheckBridge)
.checkSafeBrowsing(any(BrowserContextHandle.class));
// Updates: updated.
doAnswer(
invocation -> {
Callback<Integer> callback =
((WeakReference<Callback<Integer>>)
invocation.getArguments()[0])
.get();
callback.onResult(UpdatesState.UPDATED);
return null;
})
.when(mUpdatesDelegate)
.checkForUpdates(any(WeakReference.class));
mMediator.setInitialState();
// Passwords: compromised state.
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 20, /* breachedCount= */ 18));
// Verify the states.
assertEquals(SafeBrowsingState.UNCHECKED, mSafetyCheckModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.COMPROMISED_EXIST, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.UNCHECKED, mSafetyCheckModel.get(UPDATES_STATE));
}
@Test
public void testPasswordsInitialLoadDuringInitialState() {
// Order: setting initial state -> showing CHECK while the check is still running -> done.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 20, /* breachedCount= */ 18));
assertEquals(PasswordsState.COMPROMISED_EXIST, mPasswordCheckModel.get(PASSWORDS_STATE));
}
@Test
public void testPasswordsInitialLoadDuringRunningCheck() {
// Order: initial state -> safety check triggered -> load completed -> check done.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 20, /* breachedCount= */ 18));
assertEquals(PasswordsState.COMPROMISED_EXIST, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM,
PasswordsStatus.COMPROMISED_EXIST));
}
@Test
public void testPasswordCheckWhenRanImmediately() {
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 6, /* breachedCount= */ 3));
assertEquals(PasswordsState.COMPROMISED_EXIST, mPasswordCheckModel.get(PASSWORDS_STATE));
}
@Test
public void testPasswordsInitialLoadCheckReturnsError() {
// Order: initial state -> safety check triggered -> check error -> load ignored.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnError(
PasswordStorageType.ACCOUNT_STORAGE, new Exception("Test exception"));
assertEquals(PasswordsState.ERROR, mPasswordCheckModel.get(PASSWORDS_STATE));
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.ERROR));
}
@Test
public void testPasswordsInitialLoadUserSignedOut() {
// Order: initial state is user signed out -> should display signed out error.
mMediator.setInitialState();
setUpPasswordCheckToReturnError(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckNativeException(
"Test signed out error", PasswordCheckUIStatus.ERROR_SIGNED_OUT));
assertEquals(PasswordsState.SIGNED_OUT, mPasswordCheckModel.get(PASSWORDS_STATE));
// The results of the previous check should be ignored.
assertEquals(
1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.SIGNED_OUT));
}
@Test
public void testPasswordCheckFinishedAfterDestroy() {
mMediator.performSafetyCheck();
@PasswordsState int stateBeforeDestroy = mPasswordCheckModel.get(PASSWORDS_STATE);
mMediator.destroy();
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* totalPasswordsCount= */ 20, /* breachedCount= */ 0));
// After calling destroy() on mediator, the model is not expected to change any more.
assertEquals(stateBeforeDestroy, mPasswordCheckModel.get(PASSWORDS_STATE));
}
@Test
@Features.EnableFeatures(ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS)
public void testClickListenerStartsSignInFlowWhenUserSignedOut() {
mMediator.setInitialState();
setUpPasswordCheckToReturnError(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckNativeException(
"Test signed out error", PasswordCheckUIStatus.ERROR_SIGNED_OUT));
assertEquals(PasswordsState.SIGNED_OUT, mPasswordCheckModel.get(PASSWORDS_STATE));
click(getPasswordsClickListener(mPasswordCheckModel));
verify(mSigninLauncher)
.launchActivityIfAllowed(
any(),
eq(mProfile),
any(),
eq(SigninAndHistorySyncCoordinator.NoAccountSigninMode.ADD_ACCOUNT),
eq(
SigninAndHistorySyncCoordinator.WithAccountSigninMode
.DEFAULT_ACCOUNT_BOTTOM_SHEET),
eq(SigninAndHistorySyncCoordinator.HistoryOptInMode.NONE),
eq(SigninAccessPoint.SAFETY_CHECK));
}
@Test
public void testClickListenerLeadsToUPMAccountPasswordCheckup() {
// Order: initial state -> safety check triggered -> check done -> load completed.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* passwordsTotalCount= */ 20, /* breachedCount= */ 18));
assertEquals(PasswordsState.COMPROMISED_EXIST, mPasswordCheckModel.get(PASSWORDS_STATE));
click(getPasswordsClickListener(mPasswordCheckModel));
verify(mPasswordCheckupHelper, times(mUseGmsApi ? 1 : 0))
.getPasswordCheckupIntent(
eq(SAFETY_CHECK), eq(Optional.of(TEST_EMAIL_ADDRESS)), any(), any());
}
@Test
public void testClickListenerDontLeadToPasswordCheckupIfThereWasError() {
// Order: initial state -> safety check triggered -> check done -> load completed.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mPasswordCheckModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
setUpPasswordCheckToReturnError(
PasswordStorageType.ACCOUNT_STORAGE, new Exception("Test exception"));
assertEquals(PasswordsState.ERROR, mPasswordCheckModel.get(PASSWORDS_STATE));
assertNull(getPasswordsClickListener(mPasswordCheckModel));
}
@Test
public void testClickListenerLeadsToPasswordSettingsWhenUnchecked() {
if (!mUseGmsApi) {
Intents.init();
Intent settingsLauncherIntent = new Intent();
settingsLauncherIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
when(mSettingsLauncher.createSettingsActivityIntent(any(), anyInt(), any()))
.thenReturn(settingsLauncherIntent);
intending(anyIntent())
.respondWith(new ActivityResult(Activity.RESULT_OK, new Intent()));
}
PropertyModel passwordCheckLocalModel =
PasswordsCheckPreferenceProperties.createPasswordSafetyCheckModel("Passwords");
PropertyModel passwordCheckAccountModel =
PasswordsCheckPreferenceProperties.createPasswordSafetyCheckModel(
"Passwords for acount");
mMediator = createSafetyCheckMediator(passwordCheckAccountModel, passwordCheckLocalModel);
mMediator.setInitialState();
setUpPasswordCheckToReturnResult(
PasswordStorageType.LOCAL_STORAGE, new PasswordCheckResult(10, 0));
assertEquals(PasswordsState.UNCHECKED, passwordCheckLocalModel.get(PASSWORDS_STATE));
click(getPasswordsClickListener(passwordCheckLocalModel));
verify(mCredentialManagerLauncher, times(mUseGmsApi ? 1 : 0))
.getLocalCredentialManagerIntent(
eq(ManagePasswordsReferrer.SAFETY_CHECK), any(), any());
verify(mSettingsLauncher, times(mUseGmsApi ? 0 : 1))
.createSettingsActivityIntent(any(), anyInt(), any());
}
@Test
public void testClickListenerLeadsToUPMLocalPasswordCheckup() {
// TODO(crbug.com/41483841): Parametrize the tests in SafetyCheckMediatorTest for local and
// account storage.
// These behaviours are set here again because the tests are currently not parametrised in
// a way to support UPM for non password syncing users.
PropertyModel passwordCheckLocalModel =
PasswordsCheckPreferenceProperties.createPasswordSafetyCheckModel("Passwords");
mMediator =
createSafetyCheckMediator(
/* passwordCheckAccountModel= */ null, passwordCheckLocalModel);
when(mPasswordManagerUtilBridgeNativeMock.shouldUseUpmWiring(mSyncService, mPrefService))
.thenReturn(mUseGmsApi);
when(mSyncService.getSelectedTypes()).thenReturn(new HashSet<>());
// Order: initial state -> safety check triggered -> check done -> load completed.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, passwordCheckLocalModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, passwordCheckLocalModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.LOCAL_STORAGE,
new PasswordCheckResult(/* passwordsTotalCount= */ 20, /* breachedCount= */ 18));
assertEquals(
PasswordsState.COMPROMISED_EXIST, passwordCheckLocalModel.get(PASSWORDS_STATE));
click(getPasswordsClickListener(passwordCheckLocalModel));
verify(mPasswordCheckupHelper, times(mUseGmsApi ? 1 : 0))
.getPasswordCheckupIntent(eq(SAFETY_CHECK), eq(Optional.empty()), any(), any());
}
@Test
public void testPasswordCheckCompletesForTwoStorages() {
// Set up both local and account models
PropertyModel passwordCheckAccountModel =
PasswordsCheckPreferenceProperties.createPasswordSafetyCheckModel(
"Passwords saved on " + TEST_EMAIL_ADDRESS);
PropertyModel passwordCheckLocalModel =
PasswordsCheckPreferenceProperties.createPasswordSafetyCheckModel("Passwords");
mMediator = createSafetyCheckMediator(passwordCheckAccountModel, passwordCheckLocalModel);
// Order: initial state -> set result of the initial check -> password check -> set result
// of the password check.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, passwordCheckAccountModel.get(PASSWORDS_STATE));
assertEquals(PasswordsState.CHECKING, passwordCheckLocalModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* passwordsTotalCount= */ 20, /* breachedCount= */ 18));
assertEquals(
PasswordsState.COMPROMISED_EXIST, passwordCheckAccountModel.get(PASSWORDS_STATE));
assertEquals(PasswordsState.CHECKING, passwordCheckLocalModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.LOCAL_STORAGE,
new PasswordCheckResult(/* passwordsTotalCount= */ 20, /* breachedCount= */ 0));
assertEquals(PasswordsState.UNCHECKED, passwordCheckLocalModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, passwordCheckAccountModel.get(PASSWORDS_STATE));
assertEquals(PasswordsState.CHECKING, passwordCheckLocalModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.LOCAL_STORAGE,
new PasswordCheckResult(/* passwordsTotalCount= */ 20, /* breachedCount= */ 18));
assertEquals(
PasswordsState.COMPROMISED_EXIST, passwordCheckLocalModel.get(PASSWORDS_STATE));
assertEquals(PasswordsState.CHECKING, passwordCheckAccountModel.get(PASSWORDS_STATE));
setUpPasswordCheckToReturnResult(
PasswordStorageType.ACCOUNT_STORAGE,
new PasswordCheckResult(/* passwordsTotalCount= */ 20, /* breachedCount= */ 18));
assertEquals(
PasswordsState.COMPROMISED_EXIST, passwordCheckAccountModel.get(PASSWORDS_STATE));
}
}