chromium/chrome/android/javatests/src/org/chromium/chrome/browser/sync/ui/SyncErrorMessageTest.java

// Copyright 2021 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.ui;

import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.CoreMatchers.allOf;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.description;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

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

import android.view.ViewGroup;

import androidx.annotation.Nullable;
import androidx.test.espresso.intent.Intents;
import androidx.test.espresso.intent.matcher.IntentMatchers;
import androidx.test.filters.LargeTest;

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.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
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.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.base.test.util.Matchers;
import org.chromium.chrome.browser.SyncFirstSetupCompleteSource;
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.sync.FakeSyncServiceImpl;
import org.chromium.chrome.browser.sync.SyncTestRule;
import org.chromium.chrome.browser.sync.settings.ManageSyncSettings;
import org.chromium.chrome.browser.sync.settings.SyncSettingsUtils;
import org.chromium.chrome.browser.sync.settings.SyncSettingsUtils.SyncError;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;
import org.chromium.chrome.test.util.ChromeRenderTestRule;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.messages.MessageBannerProperties;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.signin.base.GoogleServiceAuthError;
import org.chromium.ui.modelutil.PropertyModel;

import java.io.IOException;

/** Test suites for {@link SyncErrorMessage}. */
@RunWith(ChromeJUnit4ClassRunner.class)
@DoNotBatch(reason = "TODO(crbug.com/40743432): SyncTestRule doesn't support batching.")
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class SyncErrorMessageTest {
    @Rule public final JniMocker mJniMocker = new JniMocker();

    @Mock private PasswordManagerUtilBridge.Natives mPasswordManagerUtilBridgeJniMock;
    @Mock private MessageDispatcher mMessageDispatcher;
    private FakeSyncServiceImpl mFakeSyncServiceImpl;

    @Rule
    public final SyncTestRule mSyncTestRule =
            new SyncTestRule() {
                @Override
                protected FakeSyncServiceImpl createSyncServiceImpl() {
                    return new FakeSyncServiceImpl();
                }
            };

    private static final int RENDER_TEST_REVISION = 3;
    private static final String RENDER_TEST_DESCRIPTION = "Sync error message for identity errors.";

    @Rule
    public final ChromeRenderTestRule mRenderTestRule =
            ChromeRenderTestRule.Builder.withPublicCorpus()
                    .setRevision(RENDER_TEST_REVISION)
                    .setDescription(RENDER_TEST_DESCRIPTION)
                    .setBugComponent(ChromeRenderTestRule.Component.SERVICES_SYNC)
                    .build();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mJniMocker.mock(PasswordManagerUtilBridgeJni.TEST_HOOKS, mPasswordManagerUtilBridgeJniMock);
        SyncErrorMessageImpressionTracker.resetLastShownTime();
        mFakeSyncServiceImpl = (FakeSyncServiceImpl) mSyncTestRule.getSyncService();
        SyncErrorMessage.setMessageDispatcherForTesting(mMessageDispatcher);
        doAnswer(
                        (invocation) -> {
                            PropertyModel model = invocation.getArgument(0);
                            int dismissReason = invocation.getArgument(1);
                            model.get(MessageBannerProperties.ON_DISMISSED).onResult(dismissReason);
                            return null;
                        })
                .when(mMessageDispatcher)
                .dismissMessage(any(), anyInt());
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForAuthError() throws Exception {
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setAuthError(GoogleServiceAuthError.State.INVALID_GAIA_CREDENTIALS);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setAuthError(GoogleServiceAuthError.State.NONE);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForSyncSetupIncomplete() throws Exception {
        mSyncTestRule.setUpTestAccountAndSignInWithSyncSetupAsIncomplete();
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();

        // Resolving the error should dismiss the current message.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mFakeSyncServiceImpl.setInitialSyncFeatureSetupComplete(
                            SyncFirstSetupCompleteSource.BASIC_FLOW);
                });
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForPassphraseRequired() throws Exception {
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(false);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForClientOutOfDate() throws Exception {
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setRequiresClientUpgrade(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();

        // Not possible to resolve this error from within chrome unlike the other
        // SyncErrorMessage-s.
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForTrustedVaultKeyRequired() throws Exception {
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setTrustedVaultKeyRequiredForPreferredDataTypes(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setTrustedVaultKeyRequiredForPreferredDataTypes(false);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForTrustedVaultRecoverabilityDegraded() throws Exception {
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setTrustedVaultRecoverabilityDegraded(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setTrustedVaultRecoverabilityDegraded(false);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageNotShownWhenNoError() throws Exception {
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        SyncTestUtil.waitForSyncFeatureActive();
        mFakeSyncServiceImpl.setEngineInitialized(true);

        @SyncError
        int syncError =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return SyncSettingsUtils.getSyncError(
                                    mSyncTestRule.getProfile(/* incognito= */ false));
                        });

        Assert.assertEquals(SyncError.NO_ERROR, syncError);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);

        verifyHasNeverShownMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageNotShownForUpmBackendOutdated() {
        when(mPasswordManagerUtilBridgeJniMock.isGmsCoreUpdateRequired(any(), any()))
                .thenReturn(true);
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        @SyncError
        int syncError =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            return SyncSettingsUtils.getSyncError(
                                    mSyncTestRule.getProfile(/* incognito= */ false));
                        });
        Assert.assertEquals(SyncError.UPM_BACKEND_OUTDATED, syncError);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);

        verifyHasNeverShownMessage();
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testSyncErrorMessageForAuthErrorViewModern() throws IOException {
        SyncErrorMessage.setMessageDispatcherForTesting(null);
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setAuthError(GoogleServiceAuthError.State.INVALID_GAIA_CREDENTIALS);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        ViewGroup view = mSyncTestRule.getActivity().findViewById(R.id.message_container);
        // Wait until the message ui is shown.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(view.getChildCount(), Matchers.is(1)));
        mRenderTestRule.render(view, "sync_error_message_auth_error_modern");
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testSyncErrorMessageForSyncSetupIncompleteView() throws IOException {
        SyncErrorMessage.setMessageDispatcherForTesting(null);
        mSyncTestRule.setUpTestAccountAndSignInWithSyncSetupAsIncomplete();
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        ViewGroup view = mSyncTestRule.getActivity().findViewById(R.id.message_container);
        // Wait until the message ui is shown.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(view.getChildCount(), Matchers.is(1)));
        mRenderTestRule.render(view, "sync_error_message_sync_setup_incomplete");
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testSyncErrorMessageForPassphraseRequiredView() throws IOException {
        SyncErrorMessage.setMessageDispatcherForTesting(null);
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        ViewGroup view = mSyncTestRule.getActivity().findViewById(R.id.message_container);
        // Wait until the message ui is shown.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(view.getChildCount(), Matchers.is(1)));
        mRenderTestRule.render(view, "sync_error_message_passphrase_required");
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testSyncErrorMessageForClientOutOfDateView() throws IOException {
        SyncErrorMessage.setMessageDispatcherForTesting(null);
        mSyncTestRule.setUpAccountAndEnableSyncForTesting();
        mFakeSyncServiceImpl.setRequiresClientUpgrade(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        ViewGroup view = mSyncTestRule.getActivity().findViewById(R.id.message_container);
        // Wait until the message ui is shown.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(view.getChildCount(), Matchers.is(1)));
        mRenderTestRule.render(view, "sync_error_message_client_out_of_date");
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForAuthErrorForSignedInUsers() throws Exception {
        HistogramWatcher watchIdentityErrorMessageShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorMessage.AuthError",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setAuthError(GoogleServiceAuthError.State.INVALID_GAIA_CREDENTIALS);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();
        watchIdentityErrorMessageShownHistogram.assertExpected();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setAuthError(GoogleServiceAuthError.State.NONE);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForPassphraseRequiredForSignedInUsers() throws Exception {
        HistogramWatcher watchIdentityErrorMessageShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorMessage.PassphraseRequired",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();
        watchIdentityErrorMessageShownHistogram.assertExpected();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(false);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForClientOutOfDateForSignedInUsers() throws Exception {
        HistogramWatcher watchIdentityErrorMessageShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorMessage.ClientOutOfDate",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setRequiresClientUpgrade(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();
        watchIdentityErrorMessageShownHistogram.assertExpected();

        // Not possible to resolve this error from within chrome unlike the other
        // SyncErrorMessage-s.
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForTrustedVaultKeyRequiredForSignedInUsers()
            throws Exception {
        HistogramWatcher watchIdentityErrorMessageShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorMessage.TrustedVaultKeyRequiredForPasswords",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setTrustedVaultKeyRequiredForPreferredDataTypes(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();
        watchIdentityErrorMessageShownHistogram.assertExpected();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setTrustedVaultKeyRequiredForPreferredDataTypes(false);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageShownForTrustedVaultRecoverabilityDegradedForSignedInUsers()
            throws Exception {
        HistogramWatcher watchIdentityErrorMessageShownHistogram =
                HistogramWatcher.newSingleRecordWatcher(
                        "Sync.IdentityErrorMessage.TrustedVaultRecoverabilityDegradedForPasswords",
                        SyncSettingsUtils.ErrorUiAction.SHOWN);

        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setTrustedVaultRecoverabilityDegraded(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasShownMessage();
        watchIdentityErrorMessageShownHistogram.assertExpected();

        // Resolving the error should dismiss the current message.
        mFakeSyncServiceImpl.setTrustedVaultRecoverabilityDegraded(false);
        verifyHasDismissedMessage();
    }

    @Test
    @LargeTest
    public void testSyncErrorMessageNotShownWhenNoErrorForSignedInUsers() throws Exception {
        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setAuthError(GoogleServiceAuthError.State.NONE);
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(false);
        mFakeSyncServiceImpl.setRequiresClientUpgrade(false);

        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        verifyHasNeverShownMessage();
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testSyncErrorMessageForAuthErrorViewForSignedInUsers() throws IOException {
        SyncErrorMessage.setMessageDispatcherForTesting(null);
        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setAuthError(GoogleServiceAuthError.State.INVALID_GAIA_CREDENTIALS);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        ViewGroup view = mSyncTestRule.getActivity().findViewById(R.id.message_container);
        // Wait until the message ui is shown.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(view.getChildCount(), Matchers.is(1)));
        mRenderTestRule.render(view, "identity_error_message_auth_error");
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testSyncErrorMessageForPassphraseRequiredViewForSignedInUsers() throws IOException {
        SyncErrorMessage.setMessageDispatcherForTesting(null);
        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        ViewGroup view = mSyncTestRule.getActivity().findViewById(R.id.message_container);
        // Wait until the message ui is shown.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(view.getChildCount(), Matchers.is(1)));
        mRenderTestRule.render(view, "identity_error_message_passphrase_required");
    }

    @Test
    @LargeTest
    @Feature("RenderTest")
    public void testSyncErrorMessageForClientOutOfDateViewForSignedInUsers() throws IOException {
        SyncErrorMessage.setMessageDispatcherForTesting(null);
        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setRequiresClientUpgrade(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);
        ViewGroup view = mSyncTestRule.getActivity().findViewById(R.id.message_container);
        // Wait until the message ui is shown.
        CriteriaHelper.pollUiThread(() -> Criteria.checkThat(view.getChildCount(), Matchers.is(1)));
        mRenderTestRule.render(view, "identity_error_message_client_out_of_date");
    }

    @Test
    @LargeTest
    @EnableFeatures({ChromeFeatureList.REPLACE_SYNC_PROMOS_WITH_SIGN_IN_PROMOS})
    public void testActionForPassphraseRequiredForSignedInUsers() throws Exception {
        SyncErrorMessage.setMessageDispatcherForTesting(null);

        HistogramWatcher histogramWatcher =
                HistogramWatcher.newBuilder()
                        .expectIntRecord(
                                "Sync.IdentityErrorMessage.PassphraseRequired",
                                SyncSettingsUtils.ErrorUiAction.SHOWN)
                        .expectIntRecord(
                                "Sync.IdentityErrorMessage.PassphraseRequired",
                                SyncSettingsUtils.ErrorUiAction.BUTTON_CLICKED)
                        .build();

        // Sign in.
        mSyncTestRule.setUpAccountAndSignInForTesting();
        mFakeSyncServiceImpl.setEngineInitialized(true);
        mFakeSyncServiceImpl.setPassphraseRequiredForPreferredDataTypes(true);
        mSyncTestRule.loadUrl(UrlConstants.VERSION_URL);

        Intents.init();
        onViewWaiting(allOf(withText("Enter"), isDisplayed())).perform(click());
        intended(
                IntentMatchers.hasExtra(
                        SettingsActivity.EXTRA_SHOW_FRAGMENT, ManageSyncSettings.class.getName()));
        Intents.release();

        histogramWatcher.assertExpected();
    }

    private void verifyHasShownMessage() {
        verify(
                        mMessageDispatcher,
                        description("Message should be displayed when sync error occurs."))
                .enqueueWindowScopedMessage(any(), anyBoolean());
        Assert.assertNotNull(getSyncErrorMessage());
    }

    private void verifyHasNeverShownMessage() {
        verify(
                        mMessageDispatcher,
                        never().description(
                                        "Message should be never displayed when sync error does not"
                                                + " occur."))
                .enqueueWindowScopedMessage(any(), anyBoolean());
        Assert.assertNull(getSyncErrorMessage());
    }

    private void verifyHasDismissedMessage() {
        verify(
                        mMessageDispatcher,
                        description(
                                "Message should be dismissed when sync error has been resolved."))
                .dismissMessage(any(), anyInt());
        Assert.assertNull(getSyncErrorMessage());
    }

    private @Nullable SyncErrorMessage getSyncErrorMessage() {
        return ThreadUtils.runOnUiThreadBlocking(
                () ->
                        SyncErrorMessage.getKeyForTesting()
                                .retrieveDataFromHost(
                                        mSyncTestRule
                                                .getActivity()
                                                .getWindowAndroid()
                                                .getUnownedUserDataHost()));
    }
}