chromium/chrome/browser/safety_check/android/javatests/src/org/chromium/chrome/browser/safety_check/SafetyCheckSettingsFragmentTest.java

// 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 org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import android.content.Context;

import androidx.preference.Preference;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SmallTest;

import org.hamcrest.Matchers;
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.chromium.base.CollectionUtil;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.password_check.PasswordCheck;
import org.chromium.chrome.browser.password_check.PasswordCheckFactory;
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.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.SafeBrowsingState;
import org.chromium.chrome.browser.safety_check.SafetyCheckProperties.UpdatesState;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.sync.SyncService;
import org.chromium.components.sync.UserSelectableType;
import org.chromium.ui.modelutil.PropertyModel;

import java.util.Collections;
import java.util.Set;

/** Tests {@link SafetyCheckSettingsFragment} together with {@link SafetyCheckViewBinder}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@DoNotBatch(
        reason =
                "The activity should be restarted for each test to not share saved user preferences"
                        + " between tests.")
public class SafetyCheckSettingsFragmentTest {
    private static final String TEST_EMAIL_ADDRESS = "[email protected]";
    private static final String PASSWORDS_LOCAL = "passwords_local";
    private static final String PASSWORDS_ACCOUNT = "passwords_account";
    private static final String SAFE_BROWSING = "safe_browsing";
    private static final String UPDATES = "updates";
    private static final long S_TO_MS = 1000;
    private static final long MIN_TO_MS = 60 * S_TO_MS;
    private static final long H_TO_MS = 60 * MIN_TO_MS;
    private static final long DAY_TO_MS = 24 * H_TO_MS;

    @Rule
    public SettingsActivityTestRule<SafetyCheckSettingsFragment> mSettingsActivityTestRule =
            new SettingsActivityTestRule<>(SafetyCheckSettingsFragment.class);

    @Rule public JniMocker mJniMocker = new JniMocker();

    @Mock private PasswordCheck mPasswordCheck;
    @Mock private SyncService mSyncService;
    @Mock private PasswordManagerUtilBridge.Natives mPasswordManagerUtilBridgeNativeMock;
    @Mock private PasswordManagerHelper.Natives mPasswordManagerHelperNativeMock;

    private PropertyModel mSafetyCheckModel;
    private PropertyModel mPasswordCheckPreferenceLocalModel;
    private SafetyCheckSettingsFragment mFragment;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        PasswordCheckFactory.setPasswordCheckForTesting(mPasswordCheck);
        SyncServiceFactory.setInstanceForTesting(mSyncService);
        mJniMocker.mock(
                PasswordManagerUtilBridgeJni.TEST_HOOKS, mPasswordManagerUtilBridgeNativeMock);
        mJniMocker.mock(PasswordManagerHelperJni.TEST_HOOKS, mPasswordManagerHelperNativeMock);
    }

    @Test
    @SmallTest
    public void testLastRunTimestampStrings() {
        long t0 = 12345;
        Context context = ApplicationProvider.getApplicationContext();
        // Start time not set - returns an empty string.
        assertEquals("", SafetyCheckViewBinder.getLastRunTimestampText(context, 0, 123));
        assertEquals(
                "Checked just now",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 10 * S_TO_MS));
        assertEquals(
                "Checked 1 minute ago",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + MIN_TO_MS));
        assertEquals(
                "Checked 17 minutes ago",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 17 * MIN_TO_MS));
        assertEquals(
                "Checked 1 hour ago",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + H_TO_MS));
        assertEquals(
                "Checked 13 hours ago",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 13 * H_TO_MS));
        assertEquals(
                "Checked yesterday",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + DAY_TO_MS));
        assertEquals(
                "Checked yesterday",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 2 * DAY_TO_MS - 1));
        assertEquals(
                "Checked 2 days ago",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 2 * DAY_TO_MS));
        assertEquals(
                "Checked 315 days ago",
                SafetyCheckViewBinder.getLastRunTimestampText(context, t0, t0 + 315 * DAY_TO_MS));
    }

    private void createFragmentAndModel() {
        mSettingsActivityTestRule.startSettingsActivity();
        mFragment = (SafetyCheckSettingsFragment) mSettingsActivityTestRule.getFragment();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mSafetyCheckModel =
                            SafetyCheckCoordinator.createSafetyCheckModelAndBind(mFragment);
                    mPasswordCheckPreferenceLocalModel =
                            SafetyCheckCoordinator.createPasswordCheckPreferenceModelAndBind(
                                    mFragment,
                                    mSafetyCheckModel,
                                    SafetyCheckViewBinder.PASSWORDS_KEY_LOCAL,
                                    "Passwords");
                });
    }

    private void createFragmentAndModelByBundle(boolean safetyCheckImmediateRun) {
        mSettingsActivityTestRule.startSettingsActivity(
                SafetyCheckSettingsFragment.createBundle(safetyCheckImmediateRun));
        mFragment = (SafetyCheckSettingsFragment) mSettingsActivityTestRule.getFragment();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mSafetyCheckModel =
                            SafetyCheckCoordinator.createSafetyCheckModelAndBind(mFragment);
                    mPasswordCheckPreferenceLocalModel =
                            SafetyCheckCoordinator.createPasswordCheckPreferenceModelAndBind(
                                    mFragment,
                                    mSafetyCheckModel,
                                    SafetyCheckViewBinder.PASSWORDS_KEY_LOCAL,
                                    "Passwords");
                });
    }

    private void configureMockSyncService(boolean isPasswordSyncEnabled) {
        when(mSyncService.isSyncFeatureEnabled()).thenReturn(true);
        Set<Integer> selectedTypes =
                isPasswordSyncEnabled
                        ? CollectionUtil.newHashSet(UserSelectableType.PASSWORDS)
                        : Collections.EMPTY_SET;
        when(mSyncService.getSelectedTypes()).thenReturn(selectedTypes);
        when(mSyncService.getAccountInfo())
                .thenReturn(CoreAccountInfo.createFromEmailAndGaiaId(TEST_EMAIL_ADDRESS, "0"));
        when(mPasswordManagerHelperNativeMock.hasChosenToSyncPasswords(mSyncService))
                .thenReturn(isPasswordSyncEnabled);
    }

    private void configurePasswordManagerUtilBridge(boolean usesSplitStores) {
        when(mPasswordManagerUtilBridgeNativeMock.usesSplitStoresAndUPMForLocal(any()))
                .thenReturn(usesSplitStores);
    }

    private void verifyNullStateDisplayedCorrectly(
            boolean isPasswordSyncEnabled, boolean usesSplitStores) {
        configureMockSyncService(isPasswordSyncEnabled);
        configurePasswordManagerUtilBridge(usesSplitStores);
        createFragmentAndModel();
        // Binds the account model.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    SafetyCheckCoordinator.createPasswordCheckPreferenceModelAndBind(
                            mFragment,
                            mSafetyCheckModel,
                            SafetyCheckViewBinder.PASSWORDS_KEY_ACCOUNT,
                            "Passwords for test account");
                });

        Preference passwordsLocal = mFragment.findPreference(PASSWORDS_LOCAL);
        Preference passwordsAccount = mFragment.findPreference(PASSWORDS_ACCOUNT);
        Preference safeBrowsing = mFragment.findPreference(SAFE_BROWSING);
        Preference updates = mFragment.findPreference(UPDATES);

        assertEquals(!isPasswordSyncEnabled || usesSplitStores, passwordsLocal.isVisible());
        assertEquals(isPasswordSyncEnabled, passwordsAccount.isVisible());
        assertEquals("", passwordsLocal.getSummary());
        assertEquals("", passwordsAccount.getSummary());
        assertEquals("", safeBrowsing.getSummary());
        assertEquals("", updates.getSummary());
    }

    @Test
    @MediumTest
    public void testNullStateDisplayedCorrectlySyncOffNoUsingSplitStores() {
        verifyNullStateDisplayedCorrectly(
                /* isPasswordSyncEnabled= */ false, /* usesSplitStores= */ false);
    }

    @Test
    @MediumTest
    public void testNullStateDisplayedCorrectlySyncOffUsingSplitStores() {
        verifyNullStateDisplayedCorrectly(
                /* isPasswordSyncEnabled= */ false, /* usesSplitStores= */ true);
    }

    @Test
    @MediumTest
    public void testNullStateDisplayedCorrectlySyncOnNoUsingSplitStores() {
        verifyNullStateDisplayedCorrectly(
                /* isPasswordSyncEnabled= */ true, /* usesSplitStores= */ false);
    }

    @Test
    @MediumTest
    public void testNullStateDisplayedCorrectlySyncOnUsingSplitStores() {
        verifyNullStateDisplayedCorrectly(true, true);
    }

    @Test
    @MediumTest
    public void testPasswordsCheckTitlesAreCorrect() {
        configureMockSyncService(true);
        configurePasswordManagerUtilBridge(true);
        mSettingsActivityTestRule.startSettingsActivity();
        mFragment = (SafetyCheckSettingsFragment) mSettingsActivityTestRule.getFragment();

        Preference passwordsLocal = mFragment.findPreference(PASSWORDS_LOCAL);
        Preference passwordsAccount = mFragment.findPreference(PASSWORDS_ACCOUNT);

        assertEquals(
                passwordsLocal.getTitle(),
                mFragment.getString(R.string.safety_check_passwords_local_title));
        assertEquals(
                passwordsAccount.getTitle(),
                mFragment.getString(
                        R.string.safety_check_passwords_account_title, TEST_EMAIL_ADDRESS));
    }

    @Test
    @MediumTest
    public void testStateChangeDisplayedCorrectly() {
        createFragmentAndModel();

        Preference passwordsLocal = mFragment.findPreference(PASSWORDS_LOCAL);
        Preference safeBrowsing = mFragment.findPreference(SAFE_BROWSING);
        Preference updates = mFragment.findPreference(UPDATES);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Passwords state remains unchanged.
                    // Safe browsing is in "checking".
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.SAFE_BROWSING_STATE, SafeBrowsingState.CHECKING);
                    // Updates goes through "checking" and ends up in "outdated".
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.UPDATES_STATE, UpdatesState.CHECKING);
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.UPDATES_STATE, UpdatesState.OUTDATED);
                });

        assertEquals(true, passwordsLocal.isVisible());
        assertEquals("", passwordsLocal.getSummary());
        assertEquals("", safeBrowsing.getSummary());
        assertEquals(
                ApplicationProvider.getApplicationContext()
                        .getString(R.string.safety_check_updates_outdated),
                updates.getSummary());
    }

    @Test
    @MediumTest
    public void testSafetyCheckElementsOnClick() {
        createFragmentAndModel();
        CallbackHelper passwordsLocalClicked = new CallbackHelper();
        CallbackHelper safeBrowsingClicked = new CallbackHelper();
        CallbackHelper updatesClicked = new CallbackHelper();
        // Set the listeners
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mPasswordCheckPreferenceLocalModel.set(
                            PasswordsCheckPreferenceProperties.PASSWORDS_CLICK_LISTENER,
                            (Preference.OnPreferenceClickListener)
                                    (p) -> {
                                        passwordsLocalClicked.notifyCalled();
                                        return true;
                                    });
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.SAFE_BROWSING_CLICK_LISTENER,
                            (Preference.OnPreferenceClickListener)
                                    (p) -> {
                                        safeBrowsingClicked.notifyCalled();
                                        return true;
                                    });
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.UPDATES_CLICK_LISTENER,
                            (Preference.OnPreferenceClickListener)
                                    (p) -> {
                                        updatesClicked.notifyCalled();
                                        return true;
                                    });
                });
        Preference passwordsLocal = mFragment.findPreference(PASSWORDS_LOCAL);
        Preference safeBrowsing = mFragment.findPreference(SAFE_BROWSING);
        Preference updates = mFragment.findPreference(UPDATES);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // If local password storage is available, should be clickable.
                    passwordsLocal.performClick();
                    // Safe browsing is in "checking", the element deactivates, not clickable.
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.SAFE_BROWSING_STATE, SafeBrowsingState.CHECKING);
                    safeBrowsing.performClick();
                    // Updates goes through "checking" and ends up in "outdated".
                    // Checking: the element deactivates, clicks are not handled.
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.UPDATES_STATE, UpdatesState.CHECKING);
                    // Final state: the element is reactivated and should handle clicks.
                    mSafetyCheckModel.set(
                            SafetyCheckProperties.UPDATES_STATE, UpdatesState.OUTDATED);
                    updates.performClick();
                });
        assertEquals(1, passwordsLocalClicked.getCallCount());
        assertEquals(0, safeBrowsingClicked.getCallCount());
        assertEquals(1, updatesClicked.getCallCount());
    }

    @Test
    @MediumTest
    public void testSafetyCheckDoNotImmediatelyRunByDefault() {
        createFragmentAndModelByBundle(/* safetyCheckImmediateRun= */ false);
        assertEquals(false, mFragment.shouldRunSafetyCheckImmediately());
        assertEquals(
                0,
                ChromeSharedPreferences.getInstance()
                        .readLong(
                                ChromePreferenceKeys.SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP, 0));
    }

    @Test
    @MediumTest
    public void testSafetyCheckImmediatelyRunByBundle() {
        createFragmentAndModelByBundle(/* safetyCheckImmediateRun= */ true);

        // Make sure the safety check was ran.
        CriteriaHelper.pollUiThread(
                () -> {
                    Criteria.checkThat(
                            ChromeSharedPreferences.getInstance()
                                    .readLong(
                                            ChromePreferenceKeys
                                                    .SETTINGS_SAFETY_CHECK_LAST_RUN_TIMESTAMP,
                                            0),
                            Matchers.not(0));
                });
    }
}