chromium/chrome/android/javatests/src/org/chromium/chrome/browser/password_manager/PasswordMigrationWarningExportFlowTest.java

// Copyright 2023 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.password_manager;

import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.BundleMatchers.hasEntry;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasCategories;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtras;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasType;
import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
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.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.verify;

import static org.chromium.chrome.browser.flags.ChromeFeatureList.UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING;
import static org.chromium.chrome.browser.pwd_migration.R.id.password_migration_more_options_button;
import static org.chromium.chrome.browser.pwd_migration.R.id.password_migration_next_button;
import static org.chromium.chrome.browser.pwd_migration.R.id.radio_password_export;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.app.Activity;
import android.app.Instrumentation.ActivityResult;
import android.content.Context;
import android.content.Intent;
import android.widget.Button;

import androidx.test.espresso.intent.Intents;
import androidx.test.filters.MediumTest;

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.FileUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.password_manager.PasswordMetricsUtil.HistogramExportResult;
import org.chromium.chrome.browser.password_manager.settings.ExportFlow;
import org.chromium.chrome.browser.password_manager.settings.ManualCallbackDelayer;
import org.chromium.chrome.browser.password_manager.settings.PasswordListObserver;
import org.chromium.chrome.browser.password_manager.settings.PasswordManagerHandlerProvider;
import org.chromium.chrome.browser.password_manager.settings.ReauthenticationManager;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningCoordinator;
import org.chromium.chrome.browser.pwd_migration.PasswordMigrationWarningTriggers;
import org.chromium.chrome.browser.signin.SyncConsentActivityLauncherImpl;
import org.chromium.chrome.browser.sync.settings.ManageSyncSettings;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

/** Tests for exports started from the local passwords migration warning. */
@RunWith(ChromeJUnit4ClassRunner.class)
@Batch(Batch.PER_CLASS)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@EnableFeatures(UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING)
public class PasswordMigrationWarningExportFlowTest {
    @Rule
    public ChromeTabbedActivityTestRule mChromeActivityRule = new ChromeTabbedActivityTestRule();

    private FakePasswordManagerHandler mFakePasswordManagerHandler;
    private PasswordMigrationWarningCoordinator mCoordinator;
    private ExportFlow mExportFlow;
    @Mock private PasswordStoreBridge mPasswordStoreBridge;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mChromeActivityRule.startMainActivityOnBlankPage();
        Context context = mChromeActivityRule.getActivity();
        BottomSheetController bottomSheetController =
                mChromeActivityRule
                        .getActivity()
                        .getRootUiCoordinatorForTesting()
                        .getBottomSheetController();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mExportFlow = new ExportFlow();
                    mFakePasswordManagerHandler =
                            new FakePasswordManagerHandler(
                                    PasswordManagerHandlerProvider.getForProfile(
                                            mChromeActivityRule.getProfile(false)));
                    // Create a password, otherwise the export will not be allowed when there are
                    // not passwords saved.
                    setPasswordSource("https://example.com", "test user", "password");
                    mCoordinator =
                            new PasswordMigrationWarningCoordinator(
                                    context,
                                    ProfileManager.getLastUsedRegularProfile(),
                                    bottomSheetController,
                                    SyncConsentActivityLauncherImpl.get(),
                                    ManageSyncSettings.class,
                                    mExportFlow,
                                    (PasswordListObserver observer) ->
                                            PasswordManagerHandlerProvider.getForProfile(
                                                            mChromeActivityRule.getProfile(false))
                                                    .addObserver(observer),
                                    mPasswordStoreBridge,
                                    PasswordMigrationWarningTriggers.CHROME_STARTUP,
                                    (Throwable exception) -> fail());
                    PasswordManagerHandlerProvider.getForProfile(
                                    mChromeActivityRule.getProfile(false))
                            .passwordListAvailable(1);
                    mCoordinator.showWarning();
                });
        // Go to the "More options" screen.
        onViewWaiting(allOf(withId(password_migration_more_options_button), isDisplayed()));
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Button button =
                            mChromeActivityRule
                                    .getActivity()
                                    .findViewById(password_migration_more_options_button);
                    button.performClick();
                });
    }

    /**
     * Check that the export flow ends up with sending off a share intent with the exported
     * passwords.
     */
    @Test
    @MediumTest
    @DisabledTest(message = "https://crbug.com/40925707")
    public void testExportIntent() throws Exception {
        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        Intents.init();

        requestExport();

        var histogram =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                mExportFlow.getExportEventHistogramName(),
                                ExportFlow.PasswordExportEvent.EXPORT_CONFIRMED)
                        .expectIntRecord(
                                mExportFlow.getExportResultHistogramName2ForTesting(),
                                HistogramExportResult.SUCCESS)
                        .build();

        File tempFile = null;
        File outputFile = null;
        try {
            tempFile = createFakeExportedPasswordsFile();
            // Pretend that passwords have been serialized to go directly to the intent.
            mFakePasswordManagerHandler
                    .getExportSuccessCallback()
                    .onResult(123, tempFile.getPath());

            Intent result = new Intent();
            outputFile = createFakeSavedPasswordsFile();
            result.setData(FileUtils.getUriForFile(outputFile));
            // Pretend that user has chosen to save the passwords in the file system.
            intending(hasAction(Intent.ACTION_CREATE_DOCUMENT))
                    .respondWith(new ActivityResult(Activity.RESULT_OK, result));

            onViewWaiting(
                            allOf(
                                    withText(R.string.password_settings_export_action_title),
                                    isCompletelyDisplayed()),
                            /* checkRootDialog= */ true)
                    .perform(click());

            // Assert that the expected intent was detected.
            intended(
                    allOf(
                            hasAction(equalTo(Intent.ACTION_CREATE_DOCUMENT)),
                            hasCategories(hasItem(Intent.CATEGORY_OPENABLE)),
                            hasExtras(
                                    hasEntry(
                                            Intent.EXTRA_TITLE,
                                            equalTo(
                                                    mChromeActivityRule
                                                            .getActivity()
                                                            .getResources()
                                                            .getString(
                                                                    R.string
                                                                            .password_manager_default_export_filename)))),
                            hasType("text/csv")));

            // Assert that the output file was written.
            Assert.assertTrue(outputFile.length() > 0);
            histogram.assertExpected();
        } finally {
            if (tempFile != null) {
                tempFile.delete();
            }
            if (outputFile != null) {
                outputFile.delete();
            }
        }
        Intents.release();

        onViewWaiting(
                        allOf(
                                withText(R.string.exported_passwords_delete_button),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());

        verify(mPasswordStoreBridge).clearAllPasswords();
    }

    /**
     * Check that metrics are logged when the export flow ends because there is no screen lock set
     * up.
     */
    @Test
    @MediumTest
    public void testExportFlowWithNoScreenLockRecordsMetrics() {
        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.UNAVAILABLE);

        var exportResultHistogram =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                PasswordMigrationWarningCoordinator.EXPORT_METRICS_ID
                                        + PasswordMetricsUtil.EXPORT_RESULT_HISTOGRAM_SUFFIX,
                                PasswordMetricsUtil.HistogramExportResult.NO_SCREEN_LOCK_SET_UP)
                        .build();

        requestExport();

        exportResultHistogram.assertExpected();
    }

    /**
     * Selects the export option in the local passwords migration dialog and clicks next. After that
     * the export dialog is expected to be displayed and the export will be started. It also
     * disables the timer in DialogManager which is used to allow hiding the progress bar after an
     * initial period to avoid time-dependent flakiness.
     */
    private void requestExport() {
        ReauthenticationManager.setSkipSystemReauth(true);
        onViewWaiting(allOf(withId(radio_password_export), isCompletelyDisplayed()))
                .perform(click());
        onViewWaiting(allOf(withId(password_migration_next_button), isCompletelyDisplayed()))
                .perform(click());

        // Now Chrome thinks it triggered the challenge and is waiting to be resumed. Once resumed
        // it will check the reauthentication result. First, update the reauth timestamp to indicate
        // a successful reauth:
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthenticationManager.ReauthScope.BULK);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Disable the timer for progress bar.
                    mExportFlow
                            .getDialogManagerForTesting()
                            .replaceCallbackDelayerForTesting(new ManualCallbackDelayer());
                    // Now call onResume to nudge Chrome into continuing the export flow.
                    mCoordinator.resumeExportFlow();
                });
    }

    private void setPasswordSource(String origin, String username, String password) {
        PasswordManagerHandlerProvider handlerProvider =
                ThreadUtils.runOnUiThreadBlocking(
                        () ->
                                PasswordManagerHandlerProvider.getForProfile(
                                        mChromeActivityRule.getProfile(false)));
        mFakePasswordManagerHandler.insertPasswordEntryForTesting(origin, username, password);
        ThreadUtils.runOnUiThreadBlocking(
                () ->
                        handlerProvider.setPasswordManagerHandlerForTest(
                                mFakePasswordManagerHandler));
    }

    private File createFakeExportedPasswordsFile() throws IOException {
        File passwordsDir = new File(ExportFlow.getTargetDirectory());
        // Ensure that the directory exists.
        passwordsDir.mkdir();
        File tempFile = File.createTempFile("test", ".csv", passwordsDir);
        FileWriter writer = new FileWriter(tempFile);
        writer.write("Fake serialized passwords");

        writer.close();
        return tempFile;
    }

    private File createFakeSavedPasswordsFile() throws IOException {
        File outputFile = new File(ExportFlow.getTargetDirectory(), "test_saved_passwords.csv");
        outputFile.createNewFile();
        return outputFile;
    }
}