blob: f86ef48af58aee269433f027a71601d0d46de140 [file] [log] [blame]
// Copyright 2015 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/save_passwords_collection_view_controller.h"
#include <memory>
#include <utility>
#include "base/compiler_specific.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.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"
#import "ios/chrome/browser/ui/collection_view/collection_view_controller_test.h"
#import "ios/chrome/browser/ui/settings/cells/settings_text_item.h"
#import "ios/chrome/browser/ui/settings/password_details_collection_view_controller.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/testing/wait_util.h"
#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.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;
namespace {
typedef struct {
bool export_enabled;
int section_offset;
} ExportPasswordsFeatureStatus;
class SavePasswordsCollectionViewControllerTest
: public CollectionViewControllerTest,
public ::testing::WithParamInterface<ExportPasswordsFeatureStatus> {
protected:
SavePasswordsCollectionViewControllerTest() = default;
void SetUp() override {
// TODO(crbug.com/792840): Remove parametrized tests once the feature is
// enabled.
if (GetParam().export_enabled) {
scoped_feature_list_.InitAndEnableFeature(
password_manager::features::kPasswordExport);
} else {
scoped_feature_list_.InitAndDisableFeature(
password_manager::features::kPasswordExport);
}
TestChromeBrowserState::Builder test_cbs_builder;
chrome_browser_state_ = test_cbs_builder.Build();
CollectionViewControllerTest::SetUp();
IOSChromePasswordStoreFactory::GetInstance()->SetTestingFactory(
chrome_browser_state_.get(),
&password_manager::BuildPasswordStore<web::BrowserState,
MockPasswordStore>);
CreateController();
}
MockPasswordStore& GetMockStore() {
return *static_cast<MockPasswordStore*>(
IOSChromePasswordStoreFactory::GetForBrowserState(
chrome_browser_state_.get(), ServiceAccessType::EXPLICIT_ACCESS)
.get());
}
CollectionViewController* InstantiateController() override {
return [[SavePasswordsCollectionViewController alloc]
initWithBrowserState:chrome_browser_state_.get()];
}
// Adds a form to SavePasswordsTableViewController.
void AddPasswordForm(std::unique_ptr<autofill::PasswordForm> form) {
SavePasswordsCollectionViewController* save_password_controller =
static_cast<SavePasswordsCollectionViewController*>(controller());
std::vector<std::unique_ptr<autofill::PasswordForm>> passwords;
passwords.push_back(std::move(form));
[save_password_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));
}
web::TestWebThreadBundle thread_bundle_;
std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests default case has no saved sites and no blacklisted sites.
TEST_P(SavePasswordsCollectionViewControllerTest, TestInitialization) {
CheckController();
EXPECT_EQ(GetParam().section_offset + 2, NumberOfSections());
}
// Tests adding one item in saved password section.
TEST_P(SavePasswordsCollectionViewControllerTest, AddSavedPasswords) {
AddSavedForm1();
int section_offset = GetParam().section_offset;
EXPECT_EQ(section_offset + 3, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests adding one item in blacklisted password section.
TEST_P(SavePasswordsCollectionViewControllerTest, AddBlacklistedPasswords) {
AddBlacklistedForm1();
int section_offset = GetParam().section_offset;
EXPECT_EQ(section_offset + 3, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests adding one item in saved password section, and two items in blacklisted
// password section.
TEST_P(SavePasswordsCollectionViewControllerTest, AddSavedAndBlacklisted) {
AddSavedForm1();
AddBlacklistedForm1();
AddBlacklistedForm2();
int section_offset = GetParam().section_offset;
// There should be two sections added.
EXPECT_EQ(section_offset + 4, 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_P(SavePasswordsCollectionViewControllerTest, TestSavedPasswordsOrder) {
AddSavedForm2();
CheckTextCellTitleAndSubtitle(@"example2.com", @"test@egmail.com", 2, 0);
AddSavedForm1();
CheckTextCellTitleAndSubtitle(@"example.com", @"test@egmail.com", 2, 0);
CheckTextCellTitleAndSubtitle(@"example2.com", @"test@egmail.com", 2, 1);
}
// Tests the order in which the blacklisted passwords are displayed.
TEST_P(SavePasswordsCollectionViewControllerTest,
TestBlacklistedPasswordsOrder) {
AddBlacklistedForm2();
CheckTextCellTitle(@"secret2.com", 2, 0);
AddBlacklistedForm1();
CheckTextCellTitle(@"secret.com", 2, 0);
CheckTextCellTitle(@"secret2.com", 2, 1);
}
// Tests displaying passwords in the saved passwords section when there are
// duplicates in the password store.
TEST_P(SavePasswordsCollectionViewControllerTest, AddSavedDuplicates) {
AddSavedForm1();
AddSavedForm1();
int section_offset = GetParam().section_offset;
EXPECT_EQ(section_offset + 3, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests displaying passwords in the blacklisted passwords section when there
// are duplicates in the password store.
TEST_P(SavePasswordsCollectionViewControllerTest, AddBlacklistedDuplicates) {
AddBlacklistedForm1();
AddBlacklistedForm1();
int section_offset = GetParam().section_offset;
EXPECT_EQ(section_offset + 3, NumberOfSections());
EXPECT_EQ(1, NumberOfItemsInSection(2));
}
// Tests deleting items from saved passwords and blacklisted passwords sections.
TEST_P(SavePasswordsCollectionViewControllerTest, DeleteItems) {
AddSavedForm1();
AddBlacklistedForm1();
AddBlacklistedForm2();
void (^deleteItemWithWait)(int, int) = ^(int i, int j) {
__block BOOL completionCalled = NO;
this->DeleteItem(i, j, ^{
completionCalled = YES;
});
EXPECT_TRUE(testing::WaitUntilConditionOrTimeout(
testing::kWaitForUIElementTimeout, ^bool() {
return completionCalled;
}));
};
int section_offset = GetParam().section_offset;
// Delete item in save passwords section.
deleteItemWithWait(2, 0);
EXPECT_EQ(section_offset + 3, NumberOfSections());
// 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.
deleteItemWithWait(2, 0);
EXPECT_EQ(1, NumberOfItemsInSection(2));
deleteItemWithWait(2, 0);
// There should be no password sections remaining.
EXPECT_EQ(section_offset + 2, NumberOfSections());
}
// Tests deleting items from saved passwords and blacklisted passwords sections
// when there are duplicates in the store.
TEST_P(SavePasswordsCollectionViewControllerTest, DeleteItemsWithDuplicates) {
AddSavedForm1();
AddSavedForm1();
AddBlacklistedForm1();
AddBlacklistedForm1();
AddBlacklistedForm2();
void (^deleteItemWithWait)(int, int) = ^(int i, int j) {
__block BOOL completionCalled = NO;
this->DeleteItem(i, j, ^{
completionCalled = YES;
});
EXPECT_TRUE(testing::WaitUntilConditionOrTimeout(
testing::kWaitForUIElementTimeout, ^bool() {
return completionCalled;
}));
};
int section_offset = GetParam().section_offset;
// Delete item in save passwords section.
deleteItemWithWait(2, 0);
EXPECT_EQ(section_offset + 3, NumberOfSections());
// 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.
deleteItemWithWait(2, 0);
EXPECT_EQ(1, NumberOfItemsInSection(2));
deleteItemWithWait(2, 0);
// There should be no password sections remaining.
EXPECT_EQ(section_offset + 2, NumberOfSections());
}
TEST_P(SavePasswordsCollectionViewControllerTest,
TestExportButtonDisabledNoSavedPasswords) {
if (!GetParam().export_enabled)
return;
SettingsTextItem* exportButton = GetCollectionViewItem(2, 0);
CheckTextCellTitleWithId(IDS_IOS_EXPORT_PASSWORDS, 2, 0);
UIColor* disabledColor = [[MDCPalette greyPalette] tint500];
EXPECT_NSEQ(disabledColor, 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(disabledColor, exportButton.textColor);
EXPECT_TRUE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
TEST_P(SavePasswordsCollectionViewControllerTest,
TestExportButtonEnabledWithSavedPasswords) {
if (!GetParam().export_enabled)
return;
AddSavedForm1();
SettingsTextItem* exportButton = GetCollectionViewItem(3, 0);
CheckTextCellTitleWithId(IDS_IOS_EXPORT_PASSWORDS, 3, 0);
EXPECT_NSEQ([[MDCPalette greyPalette] tint900], exportButton.textColor);
EXPECT_FALSE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
// Tests that the "Export Passwords..." button is greyed out in edit mode.
TEST_P(SavePasswordsCollectionViewControllerTest,
TestExportButtonDisabledEditMode) {
if (!GetParam().export_enabled)
return;
AddSavedForm1();
SettingsTextItem* exportButton = GetCollectionViewItem(3, 0);
CheckTextCellTitleWithId(IDS_IOS_EXPORT_PASSWORDS, 3, 0);
SavePasswordsCollectionViewController* save_passwords_controller =
static_cast<SavePasswordsCollectionViewController*>(controller());
[save_passwords_controller
collectionViewWillBeginEditing:save_passwords_controller.collectionView];
EXPECT_NSEQ([[MDCPalette greyPalette] tint500], exportButton.textColor);
EXPECT_TRUE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
// Tests that the "Export Passwords..." button is enabled after exiting
// edit mode.
TEST_P(SavePasswordsCollectionViewControllerTest,
TestExportButtonEnabledWhenEdittingFinished) {
if (!GetParam().export_enabled)
return;
AddSavedForm1();
SettingsTextItem* exportButton = GetCollectionViewItem(3, 0);
CheckTextCellTitleWithId(IDS_IOS_EXPORT_PASSWORDS, 3, 0);
SavePasswordsCollectionViewController* save_passwords_controller =
static_cast<SavePasswordsCollectionViewController*>(controller());
[save_passwords_controller
collectionViewWillBeginEditing:save_passwords_controller.collectionView];
[save_passwords_controller
collectionViewWillEndEditing:save_passwords_controller.collectionView];
EXPECT_NSEQ([[MDCPalette greyPalette] tint900], exportButton.textColor);
EXPECT_FALSE(exportButton.accessibilityTraits &
UIAccessibilityTraitNotEnabled);
}
TEST_P(SavePasswordsCollectionViewControllerTest, PropagateDeletionToStore) {
SavePasswordsCollectionViewController* save_password_controller =
static_cast<SavePasswordsCollectionViewController*>(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));
[save_password_controller deletePassword:form];
}
const std::vector<ExportPasswordsFeatureStatus> kExportFeatureStatusCases{
// Passwords export disabled
{FALSE, 0},
// Passwords export enabled
{TRUE, 1}};
INSTANTIATE_TEST_CASE_P(ExportDisabledAndEnabled,
SavePasswordsCollectionViewControllerTest,
::testing::ValuesIn(kExportFeatureStatusCases));
} // namespace