blob: e22a4b2cdbc10ba3f0bfcb8f301fbeaebbdbea49 [file] [log] [blame]
// Copyright 2018 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.
#import "ios/chrome/browser/ui/settings/passwords_table_view_controller.h"
#include "base/compiler_specific.h"
#include "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#include "components/autofill/core/common/password_form.h"
#include "components/keyed_service/core/service_access_type.h"
#include "components/password_manager/core/browser/mock_password_store.h"
#include "components/password_manager/core/browser/password_manager_test_utils.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#include "ios/chrome/browser/passwords/ios_chrome_password_store_factory.h"
#include "ios/chrome/browser/passwords/save_passwords_consumer.h"
#import "ios/chrome/browser/ui/settings/cells/settings_search_item.h"
#import "ios/chrome/browser/ui/settings/password_details_table_view_controller.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_cells_constants.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_detail_text_item.h"
#include "ios/chrome/browser/ui/table_view/chrome_table_view_controller_test.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/web/public/test/test_web_thread_bundle.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using password_manager::MockPasswordStore;
// Declaration to conformance to SavePasswordsConsumerDelegate and keep tests in
// this file working.
@interface PasswordsTableViewController (Test) <UISearchBarDelegate,
SavePasswordsConsumerDelegate>
- (void)updateExportPasswordsButton;
@end
namespace {
typedef struct {
bool export_enabled;
int section_offset;
} ExportPasswordsFeatureStatus;
class PasswordsTableViewControllerTest : public ChromeTableViewControllerTest {
protected:
PasswordsTableViewControllerTest() = default;
void SetUp() override {
TestChromeBrowserState::Builder test_cbs_builder;
chrome_browser_state_ = test_cbs_builder.Build();
ChromeTableViewControllerTest::SetUp();
IOSChromePasswordStoreFactory::GetInstance()->SetTestingFactory(
chrome_browser_state_.get(),
base::BindRepeating(
&password_manager::BuildPasswordStore<web::BrowserState,
MockPasswordStore>));
CreateController();
}
MockPasswordStore& GetMockStore() {
return *static_cast<MockPasswordStore*>(
IOSChromePasswordStoreFactory::GetForBrowserState(
chrome_browser_state_.get(), ServiceAccessType::EXPLICIT_ACCESS)
.get());
}
ChromeTableViewController* InstantiateController() override {
return [[PasswordsTableViewController alloc]
initWithBrowserState:chrome_browser_state_.get()];
}
// Adds a form to PasswordsTableViewController.
void AddPasswordForm(std::unique_ptr<autofill::PasswordForm> form) {
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
std::vector<std::unique_ptr<autofill::PasswordForm>> passwords;
passwords.push_back(std::move(form));
[passwords_controller onGetPasswordStoreResults:passwords];
}
// Creates and adds a saved password form.
void AddSavedForm1() {
auto form = std::make_unique<autofill::PasswordForm>();
form->origin = GURL("http://www.example.com/accounts/LoginAuth");
form->action = GURL("http://www.example.com/accounts/Login");
form->username_element = base::ASCIIToUTF16("Email");
form->username_value = base::ASCIIToUTF16("test@egmail.com");
form->password_element = base::ASCIIToUTF16("Passwd");
form->password_value = base::ASCIIToUTF16("test");
form->submit_element = base::ASCIIToUTF16("signIn");
form->signon_realm = "http://www.example.com/";
form->preferred = false;
form->scheme = autofill::PasswordForm::SCHEME_HTML;
form->blacklisted_by_user = false;
AddPasswordForm(std::move(form));
}
// Creates and adds a saved password form.
void AddSavedForm2() {
auto form = std::make_unique<autofill::PasswordForm>();
form->origin = GURL("http://www.example2.com/accounts/LoginAuth");
form->action = GURL("http://www.example2.com/accounts/Login");
form->username_element = base::ASCIIToUTF16("Email");
form->username_value = base::ASCIIToUTF16("test@egmail.com");
form->password_element = base::ASCIIToUTF16("Passwd");
form->password_value = base::ASCIIToUTF16("test");
form->submit_element = base::ASCIIToUTF16("signIn");
form->signon_realm = "http://www.example2.com/";
form->preferred = false;
form->scheme = autofill::PasswordForm::SCHEME_HTML;
form->blacklisted_by_user = false;
AddPasswordForm(std::move(form));
}
// Creates and adds a blacklisted site form to never offer to save
// user's password to those sites.
void AddBlacklistedForm1() {
auto form = std::make_unique<autofill::PasswordForm>();
form->origin = GURL("http://www.secret.com/login");
form->action = GURL("http://www.secret.com/action");
form->username_element = base::ASCIIToUTF16("email");
form->username_value = base::ASCIIToUTF16("test@secret.com");
form->password_element = base::ASCIIToUTF16("password");
form->password_value = base::ASCIIToUTF16("cantsay");
form->submit_element = base::ASCIIToUTF16("signIn");
form->signon_realm = "http://www.secret.com/";
form->preferred = false;
form->scheme = autofill::PasswordForm::SCHEME_HTML;
form->blacklisted_by_user = true;
AddPasswordForm(std::move(form));
}
// Creates and adds another blacklisted site form to never offer to save
// user's password to those sites.
void AddBlacklistedForm2() {
auto form = std::make_unique<autofill::PasswordForm>();
form->origin = GURL("http://www.secret2.com/login");
form->action = GURL("http://www.secret2.com/action");
form->username_element = base::ASCIIToUTF16("email");
form->username_value = base::ASCIIToUTF16("test@secret2.com");
form->password_element = base::ASCIIToUTF16("password");
form->password_value = base::ASCIIToUTF16("cantsay");
form->submit_element = base::ASCIIToUTF16("signIn");
form->signon_realm = "http://www.secret2.com/";
form->preferred = false;
form->scheme = autofill::PasswordForm::SCHEME_HTML;
form->blacklisted_by_user = true;
AddPasswordForm(std::move(form));
}
// Deletes the item at (row, section) and wait util condition returns true or
// timeout.
bool deleteItemAndWait(int section, int row, ConditionBlock condition) {
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
[passwords_controller
deleteItems:@[ [NSIndexPath indexPathForRow:row inSection:section] ]];
return base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, condition);
}
web::TestWebThreadBundle thread_bundle_;
std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
};
// Tests default case has no saved sites and no blacklisted sites.
TEST_F(PasswordsTableViewControllerTest, TestInitialization) {
CheckController();
EXPECT_EQ(2, NumberOfSections());
}
// Tests adding one item in saved password section.
TEST_F(PasswordsTableViewControllerTest, AddSavedPasswords) {
AddSavedForm1();
EXPECT_EQ(4, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests adding one item in blacklisted password section.
TEST_F(PasswordsTableViewControllerTest, AddBlacklistedPasswords) {
AddBlacklistedForm1();
EXPECT_EQ(4, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests adding one item in saved password section, and two items in blacklisted
// password section.
TEST_F(PasswordsTableViewControllerTest, AddSavedAndBlacklisted) {
AddSavedForm1();
AddBlacklistedForm1();
AddBlacklistedForm2();
// There should be two sections added.
EXPECT_EQ(5, NumberOfSections());
// There should be 1 row in saved password section.
EXPECT_EQ(1, NumberOfItemsInSection(2));
// There should be 2 rows in blacklisted password section.
EXPECT_EQ(2, NumberOfItemsInSection(3));
}
// Tests the order in which the saved passwords are displayed.
TEST_F(PasswordsTableViewControllerTest, TestSavedPasswordsOrder) {
AddSavedForm2();
CheckTextCellTextAndDetailText(@"example2.com", @"test@egmail.com", 2, 0);
AddSavedForm1();
CheckTextCellTextAndDetailText(@"example.com", @"test@egmail.com", 2, 0);
CheckTextCellTextAndDetailText(@"example2.com", @"test@egmail.com", 2, 1);
}
// Tests the order in which the blacklisted passwords are displayed.
TEST_F(PasswordsTableViewControllerTest, TestBlacklistedPasswordsOrder) {
AddBlacklistedForm2();
CheckTextCellText(@"secret2.com", 2, 0);
AddBlacklistedForm1();
CheckTextCellText(@"secret.com", 2, 0);
CheckTextCellText(@"secret2.com", 2, 1);
}
// Tests displaying passwords in the saved passwords section when there are
// duplicates in the password store.
TEST_F(PasswordsTableViewControllerTest, AddSavedDuplicates) {
AddSavedForm1();
AddSavedForm1();
EXPECT_EQ(4, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests displaying passwords in the blacklisted passwords section when there
// are duplicates in the password store.
TEST_F(PasswordsTableViewControllerTest, AddBlacklistedDuplicates) {
AddBlacklistedForm1();
AddBlacklistedForm1();
EXPECT_EQ(4, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests deleting items from saved passwords and blacklisted passwords sections.
TEST_F(PasswordsTableViewControllerTest, DeleteItems) {
AddSavedForm1();
AddBlacklistedForm1();
AddBlacklistedForm2();
// Delete item in save passwords section.
ASSERT_TRUE(deleteItemAndWait(2, 0, ^{
return NumberOfSections() == 4;
}));
// Section 2 should now be the blacklisted passwords section, and should still
// have both its items.
EXPECT_EQ(2, NumberOfItemsInSection(2));
// Delete item in blacklisted passwords section.
ASSERT_TRUE(deleteItemAndWait(2, 0, ^{
return NumberOfItemsInSection(2) == 1;
}));
// There should be no password sections remaining and no search bar.
EXPECT_TRUE(deleteItemAndWait(2, 0, ^{
return NumberOfSections() == 2;
}));
}
// Tests deleting items from saved passwords and blacklisted passwords sections
// when there are duplicates in the store.
TEST_F(PasswordsTableViewControllerTest, DeleteItemsWithDuplicates) {
AddSavedForm1();
AddSavedForm1();
AddBlacklistedForm1();
AddBlacklistedForm1();
AddBlacklistedForm2();
// Delete item in save passwords section.
ASSERT_TRUE(deleteItemAndWait(2, 0, ^{
return NumberOfSections() == 4;
}));
// Section 2 should now be the blacklisted passwords section, and should still
// have both its items.
EXPECT_EQ(2, NumberOfItemsInSection(2));
// Delete item in blacklisted passwords section.
ASSERT_TRUE(deleteItemAndWait(2, 0, ^{
return NumberOfItemsInSection(2) == 1;
}));
// There should be no password sections remaining and no search bar.
EXPECT_TRUE(deleteItemAndWait(2, 0, ^{
return NumberOfSections() == 2;
}));
}
TEST_F(PasswordsTableViewControllerTest,
TestExportButtonDisabledNoSavedPasswords) {
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
[passwords_controller updateExportPasswordsButton];
TableViewDetailTextItem* exportButton = GetTableViewItem(1, 0);
CheckTextCellTextWithId(IDS_IOS_EXPORT_PASSWORDS, 1, 0);
EXPECT_NSEQ(UIColorFromRGB(kTableViewTextLabelColorLightGrey),
exportButton.textColor);
EXPECT_TRUE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
// Add blacklisted form.
AddBlacklistedForm1();
// The export button should still be disabled as exporting blacklisted forms
// is not currently supported.
EXPECT_NSEQ(UIColorFromRGB(kTableViewTextLabelColorLightGrey),
exportButton.textColor);
EXPECT_TRUE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
TEST_F(PasswordsTableViewControllerTest,
TestExportButtonEnabledWithSavedPasswords) {
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
AddSavedForm1();
[passwords_controller updateExportPasswordsButton];
TableViewDetailTextItem* exportButton = GetTableViewItem(3, 0);
CheckTextCellTextWithId(IDS_IOS_EXPORT_PASSWORDS, 3, 0);
EXPECT_NSEQ(UIColorFromRGB(kTableViewTextLabelColorBlue),
exportButton.textColor);
EXPECT_FALSE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
// Tests that the "Export Passwords..." button is greyed out in edit mode.
TEST_F(PasswordsTableViewControllerTest, TestExportButtonDisabledEditMode) {
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
AddSavedForm1();
[passwords_controller updateExportPasswordsButton];
TableViewDetailTextItem* exportButton = GetTableViewItem(3, 0);
CheckTextCellTextWithId(IDS_IOS_EXPORT_PASSWORDS, 3, 0);
[passwords_controller setEditing:YES animated:NO];
EXPECT_NSEQ(UIColorFromRGB(kTableViewTextLabelColorLightGrey),
exportButton.textColor);
EXPECT_TRUE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
// Tests that the "Export Passwords..." button is enabled after exiting
// edit mode.
TEST_F(PasswordsTableViewControllerTest,
TestExportButtonEnabledWhenEdittingFinished) {
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
AddSavedForm1();
[passwords_controller updateExportPasswordsButton];
TableViewDetailTextItem* exportButton = GetTableViewItem(3, 0);
CheckTextCellTextWithId(IDS_IOS_EXPORT_PASSWORDS, 3, 0);
[passwords_controller setEditing:YES animated:NO];
[passwords_controller setEditing:NO animated:NO];
EXPECT_NSEQ(UIColorFromRGB(kTableViewTextLabelColorBlue),
exportButton.textColor);
EXPECT_FALSE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
TEST_F(PasswordsTableViewControllerTest, PropagateDeletionToStore) {
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
autofill::PasswordForm form;
form.origin = GURL("http://www.example.com/accounts/LoginAuth");
form.action = GURL("http://www.example.com/accounts/Login");
form.username_element = base::ASCIIToUTF16("Email");
form.username_value = base::ASCIIToUTF16("test@egmail.com");
form.password_element = base::ASCIIToUTF16("Passwd");
form.password_value = base::ASCIIToUTF16("test");
form.submit_element = base::ASCIIToUTF16("signIn");
form.signon_realm = "http://www.example.com/";
form.scheme = autofill::PasswordForm::SCHEME_HTML;
form.blacklisted_by_user = false;
AddPasswordForm(std::make_unique<autofill::PasswordForm>(form));
EXPECT_CALL(GetMockStore(), RemoveLogin(form));
[passwords_controller passwordDetailsTableViewController:nil
deletePassword:form];
}
// Tests filtering of items.
TEST_F(PasswordsTableViewControllerTest, FilterItems) {
AddSavedForm1();
AddSavedForm2();
AddBlacklistedForm1();
AddBlacklistedForm2();
EXPECT_EQ(5, NumberOfSections());
PasswordsTableViewController* passwords_controller =
static_cast<PasswordsTableViewController*>(controller());
// TODO(crbug.com/894791): Remove this comment when SettingsSearchItem is
// removed.
// Currently |bar| is not actually the UISearchBar displayed on screen, the
// displayed one is from SettingsSearchItem. Once MDC navigation bar gets
// deprecated in Settings, PasswordsTableViewController will use
// UISearchController instead and |bar| will be the real UISearchBar on
// screen.
UISearchBar* bar =
passwords_controller.navigationItem.searchController.searchBar;
// Force the initial data to be rendered into view first, before doing any
// new filtering (avoids mismatch when reloadSections is called).
[passwords_controller searchBar:bar textDidChange:@""];
// Search item in save passwords section.
[passwords_controller searchBar:bar textDidChange:@"example.com"];
// Only one item in saved passwords should remain.
EXPECT_EQ(1, NumberOfItemsInSection(2));
EXPECT_EQ(0, NumberOfItemsInSection(3));
CheckTextCellTextAndDetailText(@"example.com", @"test@egmail.com", 2, 0);
[passwords_controller searchBar:bar textDidChange:@"test@egmail.com"];
// Only two items in saved passwords should remain.
EXPECT_EQ(2, NumberOfItemsInSection(2));
EXPECT_EQ(0, NumberOfItemsInSection(3));
CheckTextCellTextAndDetailText(@"example.com", @"test@egmail.com", 2, 0);
CheckTextCellTextAndDetailText(@"example2.com", @"test@egmail.com", 2, 1);
[passwords_controller searchBar:bar textDidChange:@"secret"];
// Only two items in blacklisted should remain.
EXPECT_EQ(0, NumberOfItemsInSection(2));
EXPECT_EQ(2, NumberOfItemsInSection(3));
CheckTextCellText(@"secret.com", 3, 0);
CheckTextCellText(@"secret2.com", 3, 1);
[passwords_controller searchBar:bar textDidChange:@""];
// All items should be back.
EXPECT_EQ(2, NumberOfItemsInSection(2));
EXPECT_EQ(2, NumberOfItemsInSection(3));
}
} // namespace