Commit a4d802eb authored by Vaclav Brozek's avatar Vaclav Brozek Committed by Commit Bot

Serialise passwords for export on Android

This CL prepares the C++ binding for passing exported passwords to the
Java code controlling Chrome's settings. This consists of:

(1) Extending the PasswordUIViewAndroid C++ class to handle a call to
start serialising passwords, to spawn a task to copy and serialize the
passwords on a backend task runner, and to post the result back to
Java.

(2) Teaching the Java class SavePasswordsPreferences to trigger the
serialisation when the user taps on the Export menu item.

The changes in (1) specifically make life-time management of
PasswordUIViewAndroid a little more complex: care is take that the
class always survives the backend processing. This includes
introducing a state variable and postponing self-destruction
in Destroy().

The CL also adds a whole new unittest for PasswordUIViewAndroid and
one more scenario to the existing SavePasswordsPreferencesTest. The
code in either language is tested, but passing the information through
JNI is not, because it is not clear how that would be done.

The format changes to BUILD.gn (beyond introducing the new unittest)
are unrelated, but a result of the compulsory running of git cl format.

The CL does not introduce any processing of the serialised data in the
Java code yet (it is currently dropped on receipt). That will come in
future CLs.

Bug: 788701
Change-Id: I32bbcb4bf62883d3154b3abf81ea7b076c05e37c
Reviewed-on: https://chromium-review.googlesource.com/809127
Commit-Queue: Vaclav Brozek <vabr@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarTheresa <twellington@chromium.org>
Cr-Commit-Position: refs/heads/master@{#530603}
parent b4adc605
......@@ -4,6 +4,8 @@
package org.chromium.chrome.browser;
import org.chromium.base.Callback;
/**
* Interface for retrieving passwords and password exceptions (websites for which Chrome should not
* save password) from native code.
......@@ -61,4 +63,11 @@ public interface PasswordManagerHandler {
* @param index of exception entry.
*/
public void removeSavedPasswordException(int index);
/**
* Trigger serializing the saved passwords in the background.
*
* @param callback is called on completion, with the serialized passwords as argument.
*/
public void serializePasswords(Callback<String> callback);
}
......@@ -4,6 +4,7 @@
package org.chromium.chrome.browser;
import org.chromium.base.Callback;
import org.chromium.base.annotations.CalledByNative;
/**
......@@ -70,6 +71,11 @@ public final class PasswordUIView implements PasswordManagerHandler {
nativeHandleRemoveSavedPasswordException(mNativePasswordUIViewAndroid, index);
}
@Override
public void serializePasswords(Callback<String> callback) {
nativeHandleSerializePasswords(mNativePasswordUIViewAndroid, callback);
}
/**
* Returns the URL for the website for managing one's passwords without the need to use Chrome
* with the user's profile signed in.
......@@ -111,4 +117,6 @@ public final class PasswordUIView implements PasswordManagerHandler {
private native void nativeDestroy(long nativePasswordUIViewAndroid);
private native void nativeHandleSerializePasswords(
long nativePasswordUIViewAndroid, Callback<String> callback);
}
......@@ -42,6 +42,9 @@ public final class ReauthenticationManager {
// This allows the tests to be independent of a particular device configuration.
private static OverrideState sApiOverride = OverrideState.NOT_OVERRIDDEN;
// Used in tests to avoid displaying the OS reauth dialog.
private static boolean sSkipSystemReauth = false;
/**
* Stores the timestamp of last reauthentication of the user.
*/
......@@ -64,6 +67,11 @@ public final class ReauthenticationManager {
sApiOverride = apiOverride;
}
@VisibleForTesting
public static void setSkipSystemReauth(boolean skipSystemReauth) {
sSkipSystemReauth = skipSystemReauth;
}
/**
* Checks whether reauthentication is available on the device at all.
* @return The result of the check.
......@@ -93,6 +101,8 @@ public final class ReauthenticationManager {
*/
public static void displayReauthenticationFragment(
int descriptionId, int containerViewId, FragmentManager fragmentManager) {
if (sSkipSystemReauth) return;
Fragment passwordReauthentication = new PasswordReauthenticationFragment();
Bundle args = new Bundle();
args.putInt(PasswordReauthenticationFragment.DESCRIPTION_ID, descriptionId);
......
......@@ -21,6 +21,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.PasswordManagerHandler;
......@@ -78,6 +79,9 @@ public class SavePasswordsPreferences
// True if the user triggered the password export flow and this fragment is waiting for the
// result of the user's reauthentication.
private boolean mExportRequested;
// True if the option to export passwords in the three-dots menu should be disabled due to an
// ongoing export.
private boolean mExportOptionSuspended = false;
private Preference mLinkPref;
private ChromeSwitchPreference mSavePasswordsSwitch;
private ChromeBaseCheckBoxPreference mAutoSignInSwitch;
......@@ -109,7 +113,7 @@ public class SavePasswordsPreferences
@Override
public void onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.export_passwords).setEnabled(!mNoPasswords);
menu.findItem(R.id.export_passwords).setEnabled(!mNoPasswords && !mExportOptionSuspended);
super.onPrepareOptionsMenu(menu);
}
......@@ -117,10 +121,33 @@ public class SavePasswordsPreferences
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.export_passwords) {
// Disable re-triggering exporting until the current exporting finishes.
mExportOptionSuspended = true;
// Start fetching the serialized passwords now to use the time the user spends
// reauthenticating and reading the warning message. If the user cancels the export or
// fails the reauthentication, the serialised passwords will simply get ignored when
// they arrive.
PasswordManagerHandlerProvider.getInstance()
.getPasswordManagerHandler()
.serializePasswords(new Callback<String>() {
@Override
public void onResult(String serializedPasswords) {
// TODO(crbug.com/788701): Ensure that the SavePasswordsPreferences is
// not dead before trying to use any of its data members.
// TODO(crbug.com/788701): Ensure that the passwords are stored to a
// file here and its URI in saved-state-bundle in case the reauth
// dialogue causes this activity to be killed.
// TODO(crbug.com/788701): Synchronise with end of the UI flow and pass
// the data.
}
});
if (!ReauthenticationManager.isScreenLockSetUp(getActivity().getApplicationContext())) {
Toast.makeText(getActivity().getApplicationContext(),
R.string.password_export_set_lock_screen, Toast.LENGTH_LONG)
.show();
// Re-enable exporting, the current one was cancelled by Chrome.
mExportOptionSuspended = false;
} else if (ReauthenticationManager.authenticationStillValid()) {
exportAfterReauth();
} else {
......@@ -144,13 +171,16 @@ public class SavePasswordsPreferences
if (which == AlertDialog.BUTTON_POSITIVE) {
exportAfterWarning();
}
// Re-enable exporting, the current one was either finished or dismissed.
mExportOptionSuspended = false;
}
});
exportWarningDialogFragment.show(getFragmentManager(), null);
}
private void exportAfterWarning() {
// TODO(crbug.com/788701): Start the export.
// TODO(crbug.com/788701): Synchronise with obtaining serialised passwords and pass them to
// the intent.
}
/**
......@@ -279,7 +309,13 @@ public class SavePasswordsPreferences
super.onResume();
if (mExportRequested) {
mExportRequested = false;
if (ReauthenticationManager.authenticationStillValid()) exportAfterReauth();
// Depending on the authentication result, either carry on with exporting or re-enable
// the export menu for future attempts.
if (ReauthenticationManager.authenticationStillValid()) {
exportAfterReauth();
} else {
mExportOptionSuspended = false;
}
}
rebuildPasswordLists();
}
......
......@@ -30,6 +30,7 @@ import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Feature;
......@@ -64,12 +65,19 @@ public class SavePasswordsPreferencesTest {
private final PasswordListObserver mObserver;
// The faked contents of the password store to be displayed.
private ArrayList<SavedPasswordEntry> mSavedPasswords;
private ArrayList<SavedPasswordEntry> mSavedPasswords = new ArrayList<SavedPasswordEntry>();
// This is set to true when serializePasswords is called.
private boolean mSerializePasswordsCalled;
public void setSavedPasswords(ArrayList<SavedPasswordEntry> savedPasswords) {
mSavedPasswords = savedPasswords;
}
public boolean getSerializePasswordsCalled() {
return mSerializePasswordsCalled;
}
/**
* Constructor.
* @param PasswordListObserver The only observer.
......@@ -110,6 +118,11 @@ public class SavePasswordsPreferencesTest {
assert false;
return;
}
@Override
public void serializePasswords(Callback<String> callback) {
mSerializePasswordsCalled = true;
}
}
// Used to provide fake lists of stored passwords. Tests which need it can use setPasswordSource
......@@ -349,6 +362,35 @@ public class SavePasswordsPreferencesTest {
Espresso.onView(withContentDescription("More options")).check(doesNotExist());
}
/**
* Check that tapping the export menu requests the passwords to be serialised in the background.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportTriggersSerialization() throws Exception {
setPasswordSource(new SavedPasswordEntry("https://example.com", "test user", "password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
ReauthenticationManager.setScreenLockSetUpOverride(
ReauthenticationManager.OverrideState.AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// Before tapping the menu item for export, pretend that the last successful
// reauthentication just happened. This will allow the export flow to continue.
ReauthenticationManager.setLastReauthTimeMillis(System.currentTimeMillis());
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
Assert.assertTrue(mHandler.getSerializePasswordsCalled());
}
/**
* Check that the export menu item is included and hidden behind the overflow menu. Check that
* the menu displays the warning before letting the user export passwords.
......@@ -408,6 +450,77 @@ public class SavePasswordsPreferencesTest {
.check(matches(isDisplayed()));
}
/**
* Check that if exporting is cancelled for the absence of the screen lock, the menu item is
* enabled for a retry.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportMenuItemReenabledNoLock() throws Exception {
setPasswordSource(new SavedPasswordEntry("https://example.com", "test user", "password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
ReauthenticationManager.setScreenLockSetUpOverride(
ReauthenticationManager.OverrideState.UNAVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// The text matches a text view, but the potentially disabled entity is some wrapper two
// levels up in the view hierarchy, hence the two withParent matchers.
Espresso.onView(allOf(withText(R.string.save_password_preferences_export_action_title),
withParent(withParent(isEnabled()))))
.check(matches(isDisplayed()));
}
/**
* Check that if exporting is cancelled for the user's failure to reauthenticate, the menu item
* is enabled for a retry.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportMenuItemReenabledReauthFailure() throws Exception {
setPasswordSource(new SavedPasswordEntry("https://example.com", "test user", "password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
ReauthenticationManager.setSkipSystemReauth(true);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
// The reauthentication dialog is skipped and the last reauthentication timestamp is not
// reset. This looks like a failed reauthentication to SavePasswordsPreferences' onResume.
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
preferences.getFragmentForTest().onResume();
}
});
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// The text matches a text view, but the potentially disabled entity is some wrapper two
// levels up in the view hierarchy, hence the two withParent matchers.
Espresso.onView(allOf(withText(R.string.save_password_preferences_export_action_title),
withParent(withParent(isEnabled()))))
.check(matches(isDisplayed()));
}
/**
* Check whether the user is asked to set up a screen lock if attempting to view passwords.
*/
......
......@@ -6,16 +6,23 @@
#include <algorithm>
#include "base/android/callback_android.h"
#include "base/android/jni_string.h"
#include "base/android/jni_weak_ref.h"
#include "base/android/scoped_java_ref.h"
#include "base/bind_helpers.h"
#include "base/metrics/field_trial.h"
#include "base/strings/string_piece.h"
#include "base/task_scheduler/post_task.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "components/autofill/core/common/password_form.h"
#include "components/browser_sync/profile_sync_service.h"
#include "components/password_manager/core/browser/export/password_csv_writer.h"
#include "components/password_manager/core/browser/password_manager_constants.h"
#include "components/password_manager/core/browser/password_ui_utils.h"
#include "components/password_manager/core/browser/ui/credential_provider_interface.h"
#include "content/public/browser/browser_thread.h"
#include "jni/PasswordUIView_jni.h"
using base::android::ConvertUTF16ToJavaString;
......@@ -29,7 +36,20 @@ PasswordUIViewAndroid::PasswordUIViewAndroid(JNIEnv* env, jobject obj)
PasswordUIViewAndroid::~PasswordUIViewAndroid() {}
void PasswordUIViewAndroid::Destroy(JNIEnv*, const JavaParamRef<jobject>&) {
delete this;
switch (state_) {
case State::ALIVE:
delete this;
break;
case State::ALIVE_SERIALIZATION_PENDING:
// Postpone the deletion until the pending tasks are completed, so that
// the tasks do not attempt a use after free while reading data from
// |this|.
state_ = State::DELETION_PENDING;
break;
case State::DELETION_PENDING:
NOTREACHED();
break;
}
}
Profile* PasswordUIViewAndroid::GetProfile() {
......@@ -64,6 +84,7 @@ void PasswordUIViewAndroid::SetPasswordExceptionList(
void PasswordUIViewAndroid::UpdatePasswordLists(JNIEnv* env,
const JavaParamRef<jobject>&) {
DCHECK_EQ(State::ALIVE, state_);
password_manager_presenter_.UpdatePasswordLists();
}
......@@ -71,6 +92,7 @@ ScopedJavaLocalRef<jobject> PasswordUIViewAndroid::GetSavedPasswordEntry(
JNIEnv* env,
const JavaParamRef<jobject>&,
int index) {
DCHECK_EQ(State::ALIVE, state_);
const autofill::PasswordForm* form =
password_manager_presenter_.GetPassword(index);
if (!form) {
......@@ -91,6 +113,7 @@ ScopedJavaLocalRef<jstring> PasswordUIViewAndroid::GetSavedPasswordException(
JNIEnv* env,
const JavaParamRef<jobject>&,
int index) {
DCHECK_EQ(State::ALIVE, state_);
const autofill::PasswordForm* form =
password_manager_presenter_.GetPasswordException(index);
if (!form)
......@@ -103,6 +126,7 @@ void PasswordUIViewAndroid::HandleRemoveSavedPasswordEntry(
JNIEnv* env,
const JavaParamRef<jobject>&,
int index) {
DCHECK_EQ(State::ALIVE, state_);
password_manager_presenter_.RemoveSavedPassword(index);
}
......@@ -110,9 +134,45 @@ void PasswordUIViewAndroid::HandleRemoveSavedPasswordException(
JNIEnv* env,
const JavaParamRef<jobject>&,
int index) {
DCHECK_EQ(State::ALIVE, state_);
password_manager_presenter_.RemovePasswordException(index);
}
void PasswordUIViewAndroid::HandleSerializePasswords(
JNIEnv* env,
const base::android::JavaParamRef<jobject>&,
const base::android::JavaParamRef<jobject>& callback) {
switch (state_) {
case State::ALIVE:
state_ = State::ALIVE_SERIALIZATION_PENDING;
break;
case State::ALIVE_SERIALIZATION_PENDING:
// The UI should not allow the user to re-request export before finishing
// or cancelling the pending one.
NOTREACHED();
return;
case State::DELETION_PENDING:
// The Java part should not first request destroying of |this| and then
// ask |this| for serialized passwords.
NOTREACHED();
return;
}
// The tasks are posted with base::Unretained, because deletion is postponed
// until the reply arrives (look for the occurrences of DELETION_PENDING in
// this file). The background processing is not expected to take very long,
// but still long enough not to block the UI thread. The main concern here is
// not to avoid the background computation if |this| is about to be deleted
// but to simply avoid use after free from the background task runner.
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {base::TaskPriority::USER_VISIBLE},
base::BindOnce(&PasswordUIViewAndroid::ObtainAndSerializePasswords,
base::Unretained(this)),
base::BindOnce(
&PasswordUIViewAndroid::PostSerializedPasswords,
base::Unretained(this),
base::android::ScopedJavaGlobalRef<jobject>(env, callback)));
}
ScopedJavaLocalRef<jstring> JNI_PasswordUIView_GetAccountDashboardURL(
JNIEnv* env,
const JavaParamRef<jclass>&) {
......@@ -126,3 +186,39 @@ static jlong JNI_PasswordUIView_Init(JNIEnv* env,
PasswordUIViewAndroid* controller = new PasswordUIViewAndroid(env, obj);
return reinterpret_cast<intptr_t>(controller);
}
std::string PasswordUIViewAndroid::ObtainAndSerializePasswords() {
// This is run on a backend task runner. Do not access any member variables
// except for |credential_provider_| and |password_manager_presenter_|.
password_manager::CredentialProviderInterface* const provider =
credential_provider_for_testing_ ? credential_provider_for_testing_
: &password_manager_presenter_;
return password_manager::PasswordCSVWriter::SerializePasswords(
provider->GetAllPasswords());
}
void PasswordUIViewAndroid::PostSerializedPasswords(
const base::android::JavaRef<jobject>& callback,
std::string serialized_passwords) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
switch (state_) {
case State::ALIVE:
NOTREACHED();
break;
case State::ALIVE_SERIALIZATION_PENDING: {
state_ = State::ALIVE;
if (export_target_for_testing_) {
*export_target_for_testing_ = serialized_passwords;
} else {
JNIEnv* env = base::android::AttachCurrentThread();
base::android::RunCallbackAndroid(
callback, ConvertUTF8ToJavaString(env, serialized_passwords));
}
break;
}
case State::DELETION_PENDING:
delete this;
break;
}
}
......@@ -20,6 +20,10 @@
#include "components/password_manager/core/browser/password_store.h"
#include "components/password_manager/core/browser/password_store_consumer.h"
namespace password_manager {
class CredentialProviderInterface;
}
// PasswordUIView for Android, contains jni hooks that allows Android UI to
// display passwords and route UI commands back to native
// PasswordManagerPresenter.
......@@ -58,11 +62,67 @@ class PasswordUIViewAndroid : public PasswordUIView {
JNIEnv* env,
const base::android::JavaParamRef<jobject>&,
int index);
void HandleSerializePasswords(
JNIEnv* env,
const base::android::JavaParamRef<jobject>&,
const base::android::JavaParamRef<jobject>& callback);
// Destroy the native implementation.
void Destroy(JNIEnv*, const base::android::JavaParamRef<jobject>&);
void set_export_target_for_testing(std::string* export_target_for_testing) {
export_target_for_testing_ = export_target_for_testing;
}
void set_credential_provider_for_testing(
password_manager::CredentialProviderInterface* provider) {
credential_provider_for_testing_ = provider;
}
private:
// Possible states in the life of PasswordUIViewAndroid.
// ALIVE:
// * Destroy was not called and no background tasks are pending.
// * All data members can be used on the main task runner.
// ALIVE_SERIALIZATION_PENDING:
// * Destroy was not called, password serialization task on another task
// runner is running.
// * All data members can be used on the main task runner, except for
// |password_manager_presenter_| which can only be used inside
// ObtainAndSerializePasswords, which is being run on a backend task
// runner.
// DELETION_PENDING:
// * Destroy() was called, a background task is pending and |this| should
// be deleted once the tasks complete.
// * This state should not be reached anywhere but in the compeltion call
// of the pending task.
enum class State { ALIVE, ALIVE_SERIALIZATION_PENDING, DELETION_PENDING };
// Calls |password_manager_presenter_| to retrieve cached PasswordForm objects
// and then PasswordCSVWriter to serialize them. It returns the serialized
// value. Both steps involve a lot of memory allocation and copying, so this
// method should be executed on a suitable task runner.
std::string ObtainAndSerializePasswords();
// Sends |serialized_passwords| to Java via |callback|.
void PostSerializedPasswords(const base::android::JavaRef<jobject>& callback,
std::string serialized_passwords);
// The |state_| must only be accessed on the main task runner.
State state_ = State::ALIVE;
// If not null, PostSerializedPasswords will write the serialized passwords to
// |*export_target_for_testing_| instead of passing them to Java. This must
// remain null in production code.
std::string* export_target_for_testing_ = nullptr;
PasswordManagerPresenter password_manager_presenter_;
// If not null, passwords for exporting will be obtained from
// |*credential_provider_for_testing_|, otherwise from
// |password_manager_presenter_|. This must remain null in production code.
password_manager::CredentialProviderInterface*
credential_provider_for_testing_ = nullptr;
// Java side of UI controller.
JavaObjectWeakGlobalRef weak_java_ui_controller_;
......
// Copyright 2017 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.
#include "chrome/browser/android/password_ui_view_android.h"
#include <jni.h>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/android/jni_android.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "components/autofill/core/common/password_form.h"
#include "components/password_manager/core/browser/export/password_csv_writer.h"
#include "components/password_manager/core/browser/ui/credential_provider_interface.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
namespace android {
using autofill::PasswordForm;
using base::android::AttachCurrentThread;
using base::android::JavaParamRef;
namespace {
// Specific deleter for PasswordUIViewAndroid, which calls
// PasswordUIViewAndroid::Destroy on the object. Use this with a
// std::unique_ptr.
struct PasswordUIViewAndroidDestroyDeleter {
inline void operator()(void* ptr) const {
(static_cast<PasswordUIViewAndroid*>(ptr)->Destroy(
nullptr, JavaParamRef<jobject>(nullptr)));
}
};
class FakeCredentialProvider
: public password_manager::CredentialProviderInterface {
public:
FakeCredentialProvider() = default;
~FakeCredentialProvider() override = default;
// password_manager::CredentialProviderInterface
std::vector<std::unique_ptr<PasswordForm>> GetAllPasswords() override;
// Adds a PasswordForm specified by the arguments to the list returned by
// GetAllPasswords.
void AddPasswordEntry(const std::string& origin,
const std::string& username,
const std::string& password);
private:
std::vector<std::unique_ptr<PasswordForm>> passwords_;
DISALLOW_COPY_AND_ASSIGN(FakeCredentialProvider);
};
std::vector<std::unique_ptr<PasswordForm>>
FakeCredentialProvider::GetAllPasswords() {
std::vector<std::unique_ptr<PasswordForm>> clone;
for (const auto& password : passwords_) {
clone.push_back(std::make_unique<PasswordForm>(*password));
}
return clone;
}
void FakeCredentialProvider::AddPasswordEntry(const std::string& origin,
const std::string& username,
const std::string& password) {
auto form = std::make_unique<PasswordForm>();
form->origin = GURL(origin);
form->signon_realm = origin;
form->username_value = base::UTF8ToUTF16(username);
form->password_value = base::UTF8ToUTF16(password);
passwords_.push_back(std::move(form));
}
} // namespace
class PasswordUIViewAndroidTest : public ::testing::Test {
protected:
PasswordUIViewAndroidTest()
: testing_profile_manager_(TestingBrowserProcess::GetGlobal()),
env_(AttachCurrentThread()) {}
void SetUp() override {
ASSERT_TRUE(testing_profile_manager_.SetUp());
testing_profile_ =
testing_profile_manager_.CreateTestingProfile("test profile");
}
content::TestBrowserThreadBundle thread_bundle_;
TestingProfileManager testing_profile_manager_;
TestingProfile* testing_profile_;
JNIEnv* env_;
};
// Test that the asynchronous processing of password serialization controlled by
// PasswordUIViewAndroid arrives at the same result as synchronous way to
// serialize the passwords.
TEST_F(PasswordUIViewAndroidTest, GetSerializedPasswords) {
FakeCredentialProvider provider;
provider.AddPasswordEntry("https://example.com", "username", "password");
// Let the PasswordCSVWriter compute the result instead of hard-coding it,
// because this test focuses on PasswordUIView and not on detecting changes in
// PasswordCSVWriter.
const std::string expected_result =
password_manager::PasswordCSVWriter::SerializePasswords(
provider.GetAllPasswords());
std::unique_ptr<PasswordUIViewAndroid, PasswordUIViewAndroidDestroyDeleter>
password_ui_view(
new PasswordUIViewAndroid(env_, JavaParamRef<jobject>(nullptr)));
std::string serialized_passwords;
password_ui_view->set_export_target_for_testing(&serialized_passwords);
password_ui_view->set_credential_provider_for_testing(&provider);
password_ui_view->HandleSerializePasswords(
env_, JavaParamRef<jobject>(nullptr), JavaParamRef<jobject>(nullptr));
content::RunAllTasksUntilIdle();
EXPECT_EQ(expected_result, serialized_passwords);
}
// Test that destroying the PasswordUIView when tasks are pending does not lead
// to crashes.
TEST_F(PasswordUIViewAndroidTest, GetSerializedPasswords_Cancelled) {
FakeCredentialProvider provider;
provider.AddPasswordEntry("https://example.com", "username", "password");
std::unique_ptr<PasswordUIViewAndroid, PasswordUIViewAndroidDestroyDeleter>
password_ui_view(
new PasswordUIViewAndroid(env_, JavaParamRef<jobject>(nullptr)));
std::string serialized_passwords = "this should not get overwritten";
password_ui_view->set_export_target_for_testing(&serialized_passwords);
password_ui_view->set_credential_provider_for_testing(&provider);
password_ui_view->HandleSerializePasswords(
env_, JavaParamRef<jobject>(nullptr), JavaParamRef<jobject>(nullptr));
// Register the PasswordUIView for deletion. It should not destruct itself
// before the background tasks are run. The results of the background tasks
// are waited for and then thrown out, so |serialized_passwords| should not be
// overwritten.
password_ui_view.reset();
// Now run the background tasks (and the subsequent deletion).
content::RunAllTasksUntilIdle();
EXPECT_EQ("this should not get overwritten", serialized_passwords);
}
} // namespace android
......@@ -2167,6 +2167,7 @@ test("unit_tests") {
"../browser/android/ntp/content_suggestions_notifier_unittest.cc",
"../browser/android/oom_intervention/near_oom_monitor_unittest.cc",
"../browser/android/oom_intervention/oom_intervention_decider_unittest.cc",
"../browser/android/password_ui_view_android_unittest.cc",
"../browser/android/physical_web/eddystone_encoder_bridge_unittest.cc",
"../browser/android/physical_web/physical_web_data_source_android_unittest.cc",
"../browser/android/preferences/pref_service_bridge_unittest.cc",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment