blob: 414294adeadf527e1c3dc14dddb6090f10e2559d [file] [log] [blame]
// Copyright 2020 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.chrome.browser.safety_check;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.when;
import static org.chromium.chrome.browser.safety_check.SafetyCheckProperties.COMPROMISED_PASSWORDS;
import static org.chromium.chrome.browser.safety_check.SafetyCheckProperties.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.os.Handler;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.test.ShadowRecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
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.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.safety_check.SafetyCheckMediator.SafetyCheckInteractions;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.PasswordsState;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.SafeBrowsingState;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.UpdatesState;
import org.chromium.chrome.browser.settings.SettingsLauncher;
import org.chromium.chrome.browser.signin.ui.SigninActivityLauncher;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.ref.WeakReference;
/** Unit tests for {@link SafetyCheckMediator}. */
@RunWith(BaseRobolectricTestRunner.class)
@Features.EnableFeatures(ChromeFeatureList.SAFETY_CHECK_ANDROID)
@Config(manifest = Config.NONE, shadows = {ShadowRecordHistogram.class})
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.PasswordsResult";
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";
@Rule
public TestRule mFeaturesProcessor = new Features.JUnitProcessor();
private PropertyModel mModel;
@Mock
private SafetyCheckUpdatesDelegate mUpdatesDelegate;
@Mock
private SigninActivityLauncher mSigninLauncher;
@Mock
private SettingsLauncher mSettingsLauncher;
@Mock
private SafetyCheckBridge mBridge;
@Mock
private Handler mHandler;
@Mock
private PasswordCheck mPasswordCheck;
private SafetyCheckMediator mMediator;
private void passwordDiskDataAvailable() {
doAnswer(invocation -> {
PasswordCheck.Observer observer =
(PasswordCheck.Observer) (invocation.getArguments()[0]);
observer.onCompromisedCredentialsFetchCompleted();
observer.onSavedPasswordsFetchCompleted();
return null;
})
.when(mPasswordCheck)
.addObserver(mMediator, true);
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mModel = SafetyCheckProperties.createSafetyCheckModel();
PasswordCheckFactory.setPasswordCheckForTesting(mPasswordCheck);
mMediator = new SafetyCheckMediator(
mModel, mUpdatesDelegate, mSettingsLauncher, mSigninLauncher, mBridge, mHandler);
// 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.
when(mBridge.userSignedIn()).thenReturn(true);
// Reset the histogram count.
ShadowRecordHistogram.reset();
}
@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, mModel.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, mModel.get(UPDATES_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_UPDATES_RESULT_HISTOGRAM, UpdateStatus.OUTDATED));
}
@Test
public void testSafeBrowsingCheckEnabledStandard() {
doAnswer(invocation -> {
mMediator.onSafeBrowsingCheckResult(SafeBrowsingStatus.ENABLED_STANDARD);
return null;
})
.when(mBridge)
.checkSafeBrowsing();
mMediator.performSafetyCheck();
assertEquals(SafeBrowsingState.ENABLED_STANDARD, mModel.get(SAFE_BROWSING_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_SAFE_BROWSING_RESULT_HISTOGRAM,
SafeBrowsingStatus.ENABLED_STANDARD));
}
@Test
public void testSafeBrowsingCheckDisabled() {
doAnswer(invocation -> {
mMediator.onSafeBrowsingCheckResult(SafeBrowsingStatus.DISABLED);
return null;
})
.when(mBridge)
.checkSafeBrowsing();
mMediator.performSafetyCheck();
assertEquals(SafeBrowsingState.DISABLED, mModel.get(SAFE_BROWSING_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_SAFE_BROWSING_RESULT_HISTOGRAM, SafeBrowsingStatus.DISABLED));
}
@Test
public void testPasswordsCheckError() {
doAnswer(invocation -> {
mMediator.onPasswordCheckStatusChanged(PasswordCheckUIStatus.ERROR_UNKNOWN);
return null;
})
.when(mPasswordCheck)
.startCheck();
mMediator.performSafetyCheck();
mMediator.onCompromisedCredentialsFetchCompleted();
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.ERROR, mModel.get(PASSWORDS_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.ERROR));
}
@Test
public void testPasswordsCheckNoPasswords() {
doAnswer(invocation -> {
mMediator.onPasswordCheckStatusChanged(PasswordCheckUIStatus.ERROR_NO_PASSWORDS);
return null;
})
.when(mPasswordCheck)
.startCheck();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(0);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(0);
mMediator.performSafetyCheck();
mMediator.onCompromisedCredentialsFetchCompleted();
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.NO_PASSWORDS, mModel.get(PASSWORDS_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.NO_PASSWORDS));
}
@Test
public void testPasswordsCheckNoLeaks() {
doAnswer(invocation -> {
mMediator.onPasswordCheckStatusChanged(PasswordCheckUIStatus.IDLE);
return null;
})
.when(mPasswordCheck)
.startCheck();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(15);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(0);
mMediator.performSafetyCheck();
mMediator.onCompromisedCredentialsFetchCompleted();
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.SAFE, mModel.get(PASSWORDS_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.SAFE));
}
@Test
public void testPasswordsCheckHasLeaks() {
int numLeaks = 123;
doAnswer(invocation -> {
mMediator.onPasswordCheckStatusChanged(PasswordCheckUIStatus.IDLE);
return null;
})
.when(mPasswordCheck)
.startCheck();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(199);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(numLeaks);
mMediator.performSafetyCheck();
mMediator.onCompromisedCredentialsFetchCompleted();
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.COMPROMISED_EXIST, mModel.get(PASSWORDS_STATE));
assertEquals(numLeaks, mModel.get(COMPROMISED_PASSWORDS));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM,
PasswordsStatus.COMPROMISED_EXIST));
}
@Test
public void testNullStateLessThan10MinsPasswordsSafeState() {
// Ran just now.
SharedPreferencesManager preferenceManager = SharedPreferencesManager.getInstance();
preferenceManager.writeLong(ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis());
// Safe Browsing: on.
doAnswer(invocation -> {
mMediator.onSafeBrowsingCheckResult(SafeBrowsingStatus.ENABLED_STANDARD);
return null;
})
.when(mBridge)
.checkSafeBrowsing();
// Passwords: safe state.
passwordDiskDataAvailable();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(12);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(0);
// 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();
// Verify the states.
assertEquals(SafeBrowsingState.ENABLED_STANDARD, mModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.SAFE, mModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.OUTDATED, mModel.get(UPDATES_STATE));
}
@Test
public void testNullStateLessThan10MinsNoSavedPasswords() {
// Ran just now.
SharedPreferencesManager preferenceManager = SharedPreferencesManager.getInstance();
preferenceManager.writeLong(ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis());
// Safe Browsing: disabled by admin.
doAnswer(invocation -> {
mMediator.onSafeBrowsingCheckResult(SafeBrowsingStatus.DISABLED_BY_ADMIN);
return null;
})
.when(mBridge)
.checkSafeBrowsing();
// Passwords: no passwords.
passwordDiskDataAvailable();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(0);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(0);
// 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();
// Verify the states.
assertEquals(SafeBrowsingState.DISABLED_BY_ADMIN, mModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.NO_PASSWORDS, mModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.OFFLINE, mModel.get(UPDATES_STATE));
}
@Test
public void testNullStateLessThan10MinsPasswordsUnsafeState() {
// Ran just now.
SharedPreferencesManager preferenceManager = SharedPreferencesManager.getInstance();
preferenceManager.writeLong(ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis());
// Safe Browsing: off.
doAnswer(invocation -> {
mMediator.onSafeBrowsingCheckResult(SafeBrowsingStatus.DISABLED);
return null;
})
.when(mBridge)
.checkSafeBrowsing();
// Passwords: compromised state.
passwordDiskDataAvailable();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(20);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(18);
// 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();
// Verify the states.
assertEquals(SafeBrowsingState.DISABLED, mModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.COMPROMISED_EXIST, mModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.UPDATED, mModel.get(UPDATES_STATE));
}
@Test
public void testNullStateMoreThan10MinsPasswordsSafeState() {
// Ran 20 mins ago.
SharedPreferencesManager preferenceManager = SharedPreferencesManager.getInstance();
preferenceManager.writeLong(ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis() - (20 * 60 * 1000));
// Safe Browsing: on.
doAnswer(invocation -> {
mMediator.onSafeBrowsingCheckResult(SafeBrowsingStatus.ENABLED_STANDARD);
return null;
})
.when(mBridge)
.checkSafeBrowsing();
// Passwords: safe state.
passwordDiskDataAvailable();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(13);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(0);
// 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();
// Verify the states.
assertEquals(SafeBrowsingState.UNCHECKED, mModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.UNCHECKED, mModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.UNCHECKED, mModel.get(UPDATES_STATE));
}
@Test
public void testNullStateMoreThan10MinsPasswordsUnsafeState() {
// Ran 20 mins ago.
SharedPreferencesManager preferenceManager = SharedPreferencesManager.getInstance();
preferenceManager.writeLong(ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
System.currentTimeMillis() - (20 * 60 * 1000));
// Safe Browsing: off.
doAnswer(invocation -> {
mMediator.onSafeBrowsingCheckResult(SafeBrowsingStatus.DISABLED);
return null;
})
.when(mBridge)
.checkSafeBrowsing();
// Passwords: compromised state.
passwordDiskDataAvailable();
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(20);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(18);
// 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();
// Verify the states.
assertEquals(SafeBrowsingState.UNCHECKED, mModel.get(SAFE_BROWSING_STATE));
assertEquals(PasswordsState.COMPROMISED_EXIST, mModel.get(PASSWORDS_STATE));
assertEquals(UpdatesState.UNCHECKED, mModel.get(UPDATES_STATE));
}
@Test
public void testPasswordsInitialLoadDuringInitialState() {
// Order: initial state -> load completed -> done.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.onCompromisedCredentialsFetchCompleted();
// Not complete fetch - still checking.
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
// Data available.
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(20);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(18);
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.COMPROMISED_EXIST, mModel.get(PASSWORDS_STATE));
}
@Test
public void testPasswordsInitialLoadDuringRunningCheck() {
// Order: initial state -> safety check triggered -> load completed -> check done.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(20);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(18);
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.onCompromisedCredentialsFetchCompleted();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.onPasswordCheckStatusChanged(PasswordCheckUIStatus.IDLE);
assertEquals(PasswordsState.COMPROMISED_EXIST, mModel.get(PASSWORDS_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM,
PasswordsStatus.COMPROMISED_EXIST));
}
@Test
public void testPasswordsInitialLoadAfterRunningCheck() {
// Order: initial state -> safety check triggered -> check done -> load completed.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.onPasswordCheckStatusChanged(PasswordCheckUIStatus.IDLE);
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(20);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(18);
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.onCompromisedCredentialsFetchCompleted();
assertEquals(PasswordsState.COMPROMISED_EXIST, mModel.get(PASSWORDS_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM,
PasswordsStatus.COMPROMISED_EXIST));
}
@Test
public void testPasswordsInitialLoadCheckReturnsError() {
// Order: initial state -> safety check triggered -> check error -> load ignored.
mMediator.setInitialState();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.performSafetyCheck();
assertEquals(PasswordsState.CHECKING, mModel.get(PASSWORDS_STATE));
mMediator.onPasswordCheckStatusChanged(PasswordCheckUIStatus.ERROR_UNKNOWN);
assertEquals(PasswordsState.ERROR, mModel.get(PASSWORDS_STATE));
// Previous check found compromises.
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(20);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(18);
// The results of the previous check should be ignored.
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.ERROR, mModel.get(PASSWORDS_STATE));
mMediator.onCompromisedCredentialsFetchCompleted();
assertEquals(PasswordsState.ERROR, mModel.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 -> load ignored.
when(mBridge.userSignedIn()).thenReturn(false);
mMediator.setInitialState();
assertEquals(PasswordsState.SIGNED_OUT, mModel.get(PASSWORDS_STATE));
// Previous check found compromises.
when(mPasswordCheck.getSavedPasswordsCount()).thenReturn(20);
when(mPasswordCheck.getCompromisedCredentialsCount()).thenReturn(18);
// The results of the previous check should be ignored.
mMediator.onSavedPasswordsFetchCompleted();
assertEquals(PasswordsState.SIGNED_OUT, mModel.get(PASSWORDS_STATE));
assertEquals(1,
RecordHistogram.getHistogramValueCountForTesting(
SAFETY_CHECK_PASSWORDS_RESULT_HISTOGRAM, PasswordsStatus.SIGNED_OUT));
}
}