chromium/chrome/android/javatests/src/org/chromium/chrome/browser/sync/ManageSyncSettingsWithFakeSyncServiceImplTest.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.sync;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.core.StringStartsWith.startsWith;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.app.Activity;
import android.app.Instrumentation.ActivityResult;
import android.app.PendingIntent;

import androidx.preference.Preference;
import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.intent.matcher.IntentMatchers;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.Assert;
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.Promise;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridge;
import org.chromium.chrome.browser.password_manager.PasswordManagerUtilBridgeJni;
import org.chromium.chrome.browser.settings.SettingsActivity;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.browser.sync.settings.IdentityErrorCardPreference;
import org.chromium.chrome.browser.sync.settings.ManageSyncSettings;
import org.chromium.chrome.browser.sync.settings.SyncSettingsUtils;
import org.chromium.chrome.browser.sync.ui.PassphraseDialogFragment;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ActivityTestUtils;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.base.GoogleServiceAuthError;
import org.chromium.components.signin.test.util.FakeAccountManagerFacade;
import org.chromium.components.sync.DataType;

import java.util.Set;

/** Test for ManageSyncSettings with FakeSyncServiceImpl. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class ManageSyncSettingsWithFakeSyncServiceImplTest {
    @Rule(order = 0)
    public final SyncTestRule mSyncTestRule =
            new SyncTestRule() {
                @Override
                protected FakeSyncServiceImpl createSyncServiceImpl() {
                    return new FakeSyncServiceImpl();
                }
            };

    // SettingsActivity has to be finished before the outer ChromeTabbedActivity can be finished,
    // otherwise trying to finish ChromeTabbedActivity won't work (SyncTestRule extends
    // ChromeTabbedActivityTestRule).
    @Rule(order = 1)
    public final SettingsActivityTestRule<ManageSyncSettings> mSettingsActivityTestRule =
            new SettingsActivityTestRule<>(ManageSyncSettings.class);

    @Rule(order = 2)
    public final JniMocker mJniMocker = new JniMocker();

    private SettingsActivity mSettingsActivity;

    @Mock private PasswordManagerUtilBridge.Natives mPasswordManagerUtilBridgeJniMock;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        // Prevent "GmsCore outdated" error from being exposed in bots with old version.
        mJniMocker.mock(PasswordManagerUtilBridgeJni.TEST_HOOKS, mPasswordManagerUtilBridgeJniMock);
        when(mPasswordManagerUtilBridgeJniMock.isGmsCoreUpdateRequired(any(), any()))
                .thenReturn(false);
    }

    /** Test that triggering OnPassphraseAccepted dismisses PassphraseDialogFragment. */
    @Test
    @SmallTest
    @Feature({"Sync"})
    @DisabledTest(message = "https://crbug.com/986243")
    public void testPassphraseDialogDismissed() {
        final FakeSyncServiceImpl fakeSyncServiceImpl =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();

        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        SyncTestUtil.waitForSyncFeatureActive();
        // Trigger PassphraseDialogFragment to be shown when taping on Encryption.
        fakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(true);

        final ManageSyncSettings fragment = startManageSyncPreferences();
        Preference encryption = fragment.findPreference(ManageSyncSettings.PREF_ENCRYPTION);
        clickPreference(encryption);

        final PassphraseDialogFragment passphraseFragment =
                ActivityTestUtils.waitForFragment(
                        mSettingsActivity, ManageSyncSettings.FRAGMENT_ENTER_PASSPHRASE);
        Assert.assertTrue(passphraseFragment.isAdded());

        // Simulate OnPassphraseAccepted from external event by setting PassphraseRequired to false
        // and triggering syncStateChanged().
        // PassphraseDialogFragment should be dismissed.
        fakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(false);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    fragment.getFragmentManager().executePendingTransactions();
                    Assert.assertNull(
                            "PassphraseDialogFragment should be dismissed.",
                            mSettingsActivity
                                    .getFragmentManager()
                                    .findFragmentByTag(
                                            ManageSyncSettings.FRAGMENT_ENTER_PASSPHRASE));
                });
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testIdentityErrorCardShownForSignedInUsers() {
        // Fake an identity error.
        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        fakeSyncService.setRequiresClientUpgrade(true);

        HistogramWatcher watchIdentityErrorCardShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorCard.ClientOutOfDate",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        // The error card exists.
        onView(withId(R.id.signin_settings_card)).check(matches(isDisplayed()));
        watchIdentityErrorCardShownHistogram.assertExpected();
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testIdentityErrorCardNotShownIfNoError() {
        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        onView(withId(R.id.signin_settings_card)).check(doesNotExist());
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testIdentityErrorCardNotShownForSyncingUsers() {
        // Fake an identity error.
        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        fakeSyncService.setRequiresClientUpgrade(true);

        // Expect no records.
        HistogramWatcher watchIdentityErrorCardShownHistogram =
                HistogramWatcher.newBuilder()
                        .expectNoRecords("Sync.IdentityErrorCard.ClientOutOfDate")
                        .build();

        // Sign in, enable sync and open settings.
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();

        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        onView(withId(R.id.signin_settings_card)).check(doesNotExist());
        watchIdentityErrorCardShownHistogram.assertExpected();
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testIdentityErrorCardDynamicallyShownOnError() {
        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();

        // Expect no records initially.
        HistogramWatcher watchIdentityErrorCardShownHistogram =
                HistogramWatcher.newBuilder()
                        .expectNoRecords("Sync.IdentityErrorCard.ClientOutOfDate")
                        .build();

        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        // No error card exists right now.
        onView(withId(R.id.signin_settings_card)).check(doesNotExist());
        watchIdentityErrorCardShownHistogram.assertExpected();

        watchIdentityErrorCardShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorCard.ClientOutOfDate",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Fake an identity error.
        fakeSyncService.setRequiresClientUpgrade(true);

        // Error card is showing now.
        onViewWaiting(withId(R.id.signin_settings_card)).check(matches(isDisplayed()));
        watchIdentityErrorCardShownHistogram.assertExpected();
    }

    @Test
    @SmallTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testIdentityErrorCardDynamicallyHidden() {
        // Fake an identity error.
        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        fakeSyncService.setRequiresClientUpgrade(true);

        HistogramWatcher watchIdentityErrorCardShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorCard.ClientOutOfDate",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        IdentityErrorCardPreference preference =
                (IdentityErrorCardPreference)
                        fragment.findPreference(
                                ManageSyncSettings.PREF_IDENTITY_ERROR_CARD_PREFERENCE);

        // The error card exists right now.
        Assert.assertTrue(preference.isShown());
        onView(withId(R.id.signin_settings_card)).check(matches(isDisplayed()));
        watchIdentityErrorCardShownHistogram.assertExpected();

        // Expect no records now.
        watchIdentityErrorCardShownHistogram =
                HistogramWatcher.newBuilder()
                        .expectNoRecords("Sync.IdentityErrorCard.ClientOutOfDate")
                        .build();

        // Clear the error.
        fakeSyncService.setRequiresClientUpgrade(false);

        // The error card is now hidden.
        Assert.assertFalse(preference.isShown());
        watchIdentityErrorCardShownHistogram.assertExpected();
    }

    @Test
    @LargeTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testIdentityErrorCardActionForAuthError() throws Exception {
        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        fakeSyncService.setAuthError(GoogleServiceAuthError.State.INVALID_GAIA_CREDENTIALS);

        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();

        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        // The error card exists.
        onView(withId(R.id.signin_settings_card)).check(matches(isDisplayed()));

        FakeAccountManagerFacade fakeAccountManagerFacade =
                spy((FakeAccountManagerFacade) AccountManagerFacadeProvider.getInstance());
        AccountManagerFacadeProvider.setInstanceForTests(fakeAccountManagerFacade);

        doAnswer(
                        invocation -> {
                            // Simulate re-auth by clearing the auth error.
                            fakeSyncService.setAuthError(GoogleServiceAuthError.State.NONE);
                            return null;
                        })
                .when(fakeAccountManagerFacade)
                .updateCredentials(any(), any(), any());

        // Mimic the user tapping on the error card's button.
        onView(withId(R.id.signin_settings_card_button)).perform(click());

        // No error card exists anymore.
        onView(withId(R.id.signin_settings_card)).check(doesNotExist());
    }

    @Test
    @LargeTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testIdentityErrorCardActionForClientOutdatedError() throws Exception {
        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        fakeSyncService.setRequiresClientUpgrade(true);

        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();

        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        // The error card exists.
        onView(withId(R.id.signin_settings_card)).check(matches(isDisplayed()));

        Intents.init();
        // Stub all external intents.
        intending(IntentMatchers.anyIntent())
                .respondWith(new ActivityResult(Activity.RESULT_OK, null));

        // Mimic the user tapping on the error card's button.
        onView(withId(R.id.signin_settings_card_button)).perform(click());

        intended(IntentMatchers.hasDataString(startsWith("market")));
        Intents.release();
    }

    @Test
    @LargeTest
    @Feature({"Sync"})
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    @DisabledTest(message = "https://crbug.com/359644250")
    public void testTrustedVaultKeyRetrievalForSignedInUsers() {
        // TODO(crbug.com/334124078): Simplify the test using FakeTrustedVaultClientBackend once the
        // bug is resolved.
        TestTrustedVaultClientBackend backend = new TestTrustedVaultClientBackend();
        TrustedVaultClient.setInstanceForTesting(new TrustedVaultClient(backend));

        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        fakeSyncService.setEngineInitialized(true);
        fakeSyncService.setTrustedVaultKeyRequired(true);

        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();

        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        Preference encryption = fragment.findPreference(ManageSyncSettings.PREF_ENCRYPTION);

        // Check text summary.
        String expectedSummary = fragment.getString(R.string.identity_error_card_button_verify);
        Assert.assertEquals(encryption.getSummary().toString(), expectedSummary);

        // Mimic the user tapping on Encryption.
        clickPreference(encryption);

        CriteriaHelper.pollUiThread(() -> backend.isSuccess());
    }

    @Test
    @LargeTest
    @Feature({"Sync"})
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testSignOutUnsavedDataDialogShown() {
        final FakeSyncServiceImpl fakeSyncService =
                (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        fakeSyncService.setTypesWithUnsyncedData(Set.of(DataType.BOOKMARKS));
        // Sign in and open settings.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        ManageSyncSettings fragment = startManageSyncPreferences();
        onViewWaiting(allOf(is(fragment.getView()), isDisplayed()));

        onView(withId(R.id.recycler_view)).perform(RecyclerViewActions.scrollToLastPosition());
        onView(withText(R.string.sign_out)).perform(click());

        onView(withText(R.string.sign_out_unsaved_data_title))
                .inRoot(isDialog())
                .check(matches(isDisplayed()));
    }

    private ManageSyncSettings startManageSyncPreferences() {
        mSettingsActivity = mSettingsActivityTestRule.startSettingsActivity();
        return mSettingsActivityTestRule.getFragment();
    }

    private void clickPreference(final Preference pref) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> pref.getOnPreferenceClickListener().onPreferenceClick(pref));
        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
    }

    // An empty implementation to test only the fact that "something" happens when the encryption
    // dialog is clicked.
    public static class TestTrustedVaultClientBackend extends TrustedVaultClient.EmptyBackend {
        private boolean mSuccess;

        public TestTrustedVaultClientBackend() {
            mSuccess = false;
        }

        public boolean isSuccess() {
            return mSuccess;
        }

        @Override
        public Promise<PendingIntent> createKeyRetrievalIntent(CoreAccountInfo accountInfo) {
            mSuccess = true;
            return super.createKeyRetrievalIntent(accountInfo);
        }
    }
}