blob: 10bd6a8d0f476f224a51fc17cedfbbb1613836e3 [file] [log] [blame]
// Copyright 2014 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.
package org.chromium.chrome.browser.preferences.password;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.withParent;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Espresso;
import android.support.test.filters.SmallTest;
import android.view.View;
import org.junit.Assert;
import org.junit.Rule;
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;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.preferences.ChromeBaseCheckBoxPreference;
import org.chromium.chrome.browser.preferences.ChromeSwitchPreference;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.preferences.Preferences;
import org.chromium.chrome.browser.preferences.PreferencesTest;
import org.chromium.chrome.browser.test.ChromeBrowserTestRule;
import org.chromium.chrome.test.util.browser.Features;
import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
import java.util.ArrayList;
/**
* Tests for the "Save Passwords" settings screen.
*/
@RunWith(BaseJUnit4ClassRunner.class)
public class SavePasswordsPreferencesTest {
@Rule
public final ChromeBrowserTestRule mBrowserTestRule = new ChromeBrowserTestRule();
@Rule
public TestRule mProcessor = new Features.InstrumentationProcessor();
private static final class FakePasswordManagerHandler implements PasswordManagerHandler {
// This class has exactly one observer, set on construction and expected to last at least as
// long as this object (a good candidate is the owner of this object).
private final PasswordListObserver mObserver;
// The faked contents of the password store to be displayed.
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.
*/
public FakePasswordManagerHandler(PasswordListObserver observer) {
mObserver = observer;
}
// Pretends that the updated lists are |mSavedPasswords| for the saved passwords and an
// empty list for exceptions and immediately calls the observer.
@Override
public void updatePasswordLists() {
mObserver.passwordListAvailable(mSavedPasswords.size());
}
@Override
public SavedPasswordEntry getSavedPasswordEntry(int index) {
return mSavedPasswords.get(index);
}
@Override
public String getSavedPasswordException(int index) {
// Define this method before starting to use it in tests.
assert false;
return null;
}
@Override
public void removeSavedPasswordEntry(int index) {
// Define this method before starting to use it in tests.
assert false;
return;
}
@Override
public void removeSavedPasswordException(int index) {
// Define this method before starting to use it in tests.
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
// to instantiate it.
FakePasswordManagerHandler mHandler;
/**
* Helper to set up a fake source of displayed passwords.
* @param entry An entry to be added to saved passwords. Can be null.
*/
private void setPasswordSource(SavedPasswordEntry entry) throws Exception {
if (mHandler == null) {
mHandler = new FakePasswordManagerHandler(PasswordManagerHandlerProvider.getInstance());
}
ArrayList<SavedPasswordEntry> entries = new ArrayList<SavedPasswordEntry>();
if (entry != null) entries.add(entry);
mHandler.setSavedPasswords(entries);
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
PasswordManagerHandlerProvider.getInstance().setPasswordManagerHandlerForTest(
mHandler);
}
});
}
/**
* Ensure that resetting of empty passwords list works.
*/
@Test
@SmallTest
@Feature({"Preferences"})
public void testResetListEmpty() throws Exception {
// Load the preferences, they should show the empty list.
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
SavePasswordsPreferences savePasswordPreferences =
(SavePasswordsPreferences) preferences.getFragmentForTest();
// Emulate an update from PasswordStore. This should not crash.
savePasswordPreferences.passwordListAvailable(0);
}
});
}
/**
* Ensure that the on/off switch in "Save Passwords" settings actually enables and disables
* password saving.
*/
@Test
@SmallTest
@Feature({"Preferences"})
public void testSavePasswordsSwitch() throws Exception {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
PrefServiceBridge.getInstance().setRememberPasswordsEnabled(true);
}
});
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
SavePasswordsPreferences savedPasswordPrefs =
(SavePasswordsPreferences) preferences.getFragmentForTest();
ChromeSwitchPreference onOffSwitch = (ChromeSwitchPreference)
savedPasswordPrefs.findPreference(
SavePasswordsPreferences.PREF_SAVE_PASSWORDS_SWITCH);
Assert.assertTrue(onOffSwitch.isChecked());
PreferencesTest.clickPreference(savedPasswordPrefs, onOffSwitch);
Assert.assertFalse(PrefServiceBridge.getInstance().isRememberPasswordsEnabled());
PreferencesTest.clickPreference(savedPasswordPrefs, onOffSwitch);
Assert.assertTrue(PrefServiceBridge.getInstance().isRememberPasswordsEnabled());
preferences.finish();
PrefServiceBridge.getInstance().setRememberPasswordsEnabled(false);
}
});
final Preferences preferences2 =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
SavePasswordsPreferences savedPasswordPrefs =
(SavePasswordsPreferences) preferences2.getFragmentForTest();
ChromeSwitchPreference onOffSwitch = (ChromeSwitchPreference)
savedPasswordPrefs.findPreference(
SavePasswordsPreferences.PREF_SAVE_PASSWORDS_SWITCH);
Assert.assertFalse(onOffSwitch.isChecked());
}
});
}
/**
* Ensure that the "Auto Sign-in" switch in "Save Passwords" settings actually enables and
* disables auto sign-in.
*/
@Test
@SmallTest
@Feature({"Preferences"})
public void testAutoSignInCheckbox() throws Exception {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
PrefServiceBridge.getInstance().setPasswordManagerAutoSigninEnabled(true);
}
});
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
SavePasswordsPreferences passwordPrefs =
(SavePasswordsPreferences) preferences.getFragmentForTest();
ChromeBaseCheckBoxPreference onOffSwitch =
(ChromeBaseCheckBoxPreference) passwordPrefs.findPreference(
SavePasswordsPreferences.PREF_AUTOSIGNIN_SWITCH);
Assert.assertTrue(onOffSwitch.isChecked());
PreferencesTest.clickPreference(passwordPrefs, onOffSwitch);
Assert.assertFalse(
PrefServiceBridge.getInstance().isPasswordManagerAutoSigninEnabled());
PreferencesTest.clickPreference(passwordPrefs, onOffSwitch);
Assert.assertTrue(
PrefServiceBridge.getInstance().isPasswordManagerAutoSigninEnabled());
preferences.finish();
PrefServiceBridge.getInstance().setPasswordManagerAutoSigninEnabled(false);
}
});
final Preferences preferences2 =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
SavePasswordsPreferences passwordPrefs =
(SavePasswordsPreferences) preferences2.getFragmentForTest();
ChromeBaseCheckBoxPreference onOffSwitch =
(ChromeBaseCheckBoxPreference) passwordPrefs.findPreference(
SavePasswordsPreferences.PREF_AUTOSIGNIN_SWITCH);
Assert.assertFalse(onOffSwitch.isChecked());
}
});
}
/**
* Check that if there are no saved passwords, the export menu item is disabled.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportMenuDisabled() throws Exception {
// Ensure there are no saved passwords reported to settings.
setPasswordSource(null);
ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// The text matches a text view, but the 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(not(isEnabled())))))
.check(matches(isDisplayed()));
}
/**
* Check that if there are saved passwords, the export menu item is enabled.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportMenuEnabled() throws Exception {
setPasswordSource(new SavedPasswordEntry("https://example.com", "test user", "password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
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 "PasswordExport" feature is not explicitly enabled, there is no menu item to
* export passwords.
* TODO(crbug.com/788701): Add the @DisableFeatures annotation once exporting gets enabled by
* default, and remove completely once the feature is gone.
*/
@Test
@SmallTest
@Feature({"Preferences"})
public void testExportMenuMissing() throws Exception {
ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
// Ideally this would need the same matcher (Espresso.OVERFLOW_BUTTON_MATCHER) as used
// inside Espresso.openActionBarOverflowOrOptionsMenu(), but that is private to Espresso.
// Matching the overflow menu with the class name "OverflowMenuButton" won't work on
// obfuscated release builds, so matching the description remains. The
// OVERFLOW_BUTTON_MATCHER specifies the string directly, not via string resource, so this
// is also done below.
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.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportMenuItem() 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());
Espresso.onView(withText(R.string.settings_passwords_export_description))
.check(matches(isDisplayed()));
}
/**
* Check whether the user is asked to set up a screen lock if attempting to export passwords.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportMenuItemNoLock() 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());
View mainDecorView = preferences.getWindow().getDecorView();
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
Espresso.onView(withText(R.string.password_export_set_lock_screen))
.inRoot(withDecorView(not(is(mainDecorView))))
.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.
*/
@Test
@SmallTest
@Feature({"Preferences"})
public void testViewPasswordNoLock() 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());
View mainDecorView = preferences.getWindow().getDecorView();
Espresso.onView(withText(containsString("test user"))).perform(click());
Espresso.onView(withContentDescription(R.string.password_entry_editor_copy_stored_password))
.perform(click());
Espresso.onView(withText(R.string.password_entry_editor_set_lock_screen))
.inRoot(withDecorView(not(is(mainDecorView))))
.check(matches(isDisplayed()));
}
/**
* Check whether the user can view a saved password.
*/
@Test
@SmallTest
@Feature({"Preferences"})
public void testViewPassword() throws Exception {
setPasswordSource(
new SavedPasswordEntry("https://example.com", "test user", "test password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
ReauthenticationManager.setScreenLockSetUpOverride(
ReauthenticationManager.OverrideState.AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
Espresso.onView(withText(containsString("test user"))).perform(click());
// Before tapping the view button, pretend that the last successful reauthentication just
// happened. This will allow the export flow to continue.
ReauthenticationManager.setLastReauthTimeMillis(System.currentTimeMillis());
Espresso.onView(withContentDescription(R.string.password_entry_editor_view_stored_password))
.perform(click());
Espresso.onView(withText("test password")).check(matches(isDisplayed()));
}
}