blob: 317179587f771ea9f91ce4b29d3a942c37eb1c78 [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.
#include "chrome/browser/password_manager/password_accessory_controller.h"
#include <memory>
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/password_manager/password_accessory_view_interface.h"
#include "chrome/browser/password_manager/password_generation_dialog_view_interface.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/autofill/core/common/password_form.h"
#include "components/autofill/core/common/password_generation_util.h"
#include "components/autofill/core/common/signatures_util.h"
#include "components/password_manager/core/browser/password_generation_manager.h"
#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/password_manager/core/browser/stub_password_manager_driver.h"
#include "components/strings/grit/components_strings.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
using autofill::PasswordForm;
using autofill::password_generation::PasswordGenerationUIData;
using base::ASCIIToUTF16;
using base::UTF16ToASCII;
using base::UTF16ToWide;
using testing::_;
using testing::ByMove;
using testing::ElementsAre;
using testing::Mock;
using testing::NotNull;
using testing::PrintToString;
using testing::Return;
using testing::StrictMock;
using AccessoryItem = PasswordAccessoryViewInterface::AccessoryItem;
using ItemType = AccessoryItem::Type;
constexpr char kExampleSite[] = "https://example.com";
constexpr char kExampleDomain[] = "example.com";
// The mock view mocks the platform-specific implementation. That also means
// that we have to care about the lifespan of the Controller because that would
// usually be responsibility of the view.
class MockPasswordAccessoryView : public PasswordAccessoryViewInterface {
public:
MockPasswordAccessoryView() = default;
MOCK_METHOD2(OnItemsAvailable,
void(const GURL& origin,
const std::vector<AccessoryItem>& items));
MOCK_METHOD1(OnFillingTriggered, void(const base::string16& textToFill));
MOCK_METHOD0(OnViewDestroyed, void());
MOCK_METHOD1(OnAutomaticGenerationStatusChanged, void(bool));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordAccessoryView);
};
class MockPasswordManagerDriver
: public password_manager::StubPasswordManagerDriver {
public:
MockPasswordManagerDriver() = default;
MOCK_METHOD0(GetPasswordGenerationManager,
password_manager::PasswordGenerationManager*());
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordManagerDriver);
};
class MockPasswordGenerationManager
: public password_manager::PasswordGenerationManager {
public:
MockPasswordGenerationManager(password_manager::PasswordManagerClient* client,
password_manager::PasswordManagerDriver* driver)
: password_manager::PasswordGenerationManager(client, driver) {}
MOCK_METHOD5(GeneratePassword,
base::string16(const GURL&,
autofill::FormSignature,
autofill::FieldSignature,
uint32_t,
uint32_t*));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordGenerationManager);
};
// Mock modal dialog view used to bypass the need of a valid top level window.
class MockPasswordGenerationDialogView
: public PasswordGenerationDialogViewInterface {
public:
MockPasswordGenerationDialogView() = default;
MOCK_METHOD1(Show, void(base::string16&));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordGenerationDialogView);
};
// Pretty prints input for the |MatchesItem| matcher.
std::string PrintItem(const base::string16& text,
const base::string16& description,
bool is_password,
ItemType type) {
return "has text \"" + base::UTF16ToASCII(text) + "\" and description \"" +
base::UTF16ToASCII(description) + "\" and is " +
(is_password ? "" : "not ") +
"a password "
"and type is " +
PrintToString(static_cast<int>(type));
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER_P(MatchesLabel, text, PrintItem(text, text, false, ItemType::LABEL)) {
return arg.text == text && arg.is_password == false &&
arg.content_description == text && arg.itemType == ItemType::LABEL;
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER(IsDivider, "is a divider") {
return arg.text.empty() && arg.is_password == false &&
arg.content_description.empty() && arg.itemType == ItemType::DIVIDER;
}
// Compares whether a given AccessoryItem is a label with the given text.
MATCHER_P(MatchesOption, text, PrintItem(text, text, false, ItemType::OPTION)) {
return arg.text == text && arg.is_password == false &&
arg.content_description == text && arg.itemType == ItemType::OPTION;
}
// Compares whether a given AccessoryItem had the given properties.
MATCHER_P4(MatchesItem,
text,
description,
is_password,
itemType,
PrintItem(text, description, is_password, itemType)) {
return arg.text == text && arg.is_password == is_password &&
arg.content_description == description && arg.itemType == itemType;
}
// Creates a new map entry in the |first| element of the returned pair. The
// |second| element holds the PasswordForm that the |first| element points to.
// That way, the pointer only points to a valid address in the called scope.
std::pair<std::pair<base::string16, const PasswordForm*>,
std::unique_ptr<const PasswordForm>>
CreateEntry(const std::string& username, const std::string& password) {
PasswordForm form;
form.username_value = ASCIIToUTF16(username);
form.password_value = ASCIIToUTF16(password);
std::unique_ptr<const PasswordForm> form_ptr(
new PasswordForm(std::move(form)));
auto username_form_pair =
std::make_pair(ASCIIToUTF16(username), form_ptr.get());
return {std::move(username_form_pair), std::move(form_ptr)};
}
base::string16 password_for_str(const base::string16& user) {
return l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_DESCRIPTION, user);
}
base::string16 password_for_str(const std::string& user) {
return password_for_str(ASCIIToUTF16(user));
}
base::string16 passwords_empty_str(const std::string& domain) {
return l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_EMPTY_MESSAGE,
ASCIIToUTF16(domain));
}
base::string16 passwords_title_str(const std::string& domain) {
return l10n_util::GetStringFUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_PASSWORD_LIST_TITLE, ASCIIToUTF16(domain));
}
base::string16 no_user_str() {
return l10n_util::GetStringUTF16(IDS_PASSWORD_MANAGER_EMPTY_LOGIN);
}
base::string16 manage_passwords_str() {
return l10n_util::GetStringUTF16(
IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK);
}
PasswordGenerationUIData GetTestGenerationUIData1() {
PasswordForm form;
form.form_data = autofill::FormData();
form.form_data.action = GURL("http://www.example1.com/accounts/Login");
form.form_data.origin = GURL("http://www.example1.com/accounts/LoginAuth");
PasswordGenerationUIData data;
data.password_form = form;
data.generation_element = ASCIIToUTF16("testelement1");
data.max_length = 10;
return data;
}
PasswordGenerationUIData GetTestGenerationUIData2() {
PasswordForm form;
form.form_data = autofill::FormData();
form.form_data.action = GURL("http://www.example2.com/accounts/Login");
form.form_data.origin = GURL("http://www.example2.com/accounts/LoginAuth");
PasswordGenerationUIData data;
data.password_form = form;
data.generation_element = ASCIIToUTF16("testelement2");
data.max_length = 11;
return data;
}
} // namespace
// Automagically used to pretty-print AccessoryItems. Must be in same namespace.
void PrintTo(const AccessoryItem& item, std::ostream* os) {
*os << "has text \"" << UTF16ToWide(item.text) << "\" and description \""
<< UTF16ToWide(item.content_description) << "\" and is "
<< (item.is_password ? "" : "not ") << "a password and type is "
<< PrintToString(static_cast<int>(item.itemType));
}
// Automagically used to pretty-print item vectors. Must be in same namespace.
void PrintTo(const std::vector<AccessoryItem>& items, std::ostream* os) {
*os << "has " << items.size() << " elements where\n";
for (size_t index = 0; index < items.size(); ++index) {
*os << "element #" << index << " ";
PrintTo(items[index], os);
*os << "\n";
}
}
class PasswordAccessoryControllerTest : public ChromeRenderViewHostTestHarness {
public:
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
NavigateAndCommit(GURL(kExampleSite));
PasswordAccessoryController::CreateForWebContentsForTesting(
web_contents(),
std::make_unique<StrictMock<MockPasswordAccessoryView>>(),
mock_dialog_factory_.Get());
NavigateAndCommit(GURL("https://example.com"));
}
PasswordAccessoryController* controller() {
return PasswordAccessoryController::FromWebContents(web_contents());
}
MockPasswordAccessoryView* view() {
return static_cast<MockPasswordAccessoryView*>(controller()->view());
}
const base::MockCallback<PasswordAccessoryController::CreateDialogFactory>&
mock_dialog_factory() {
return mock_dialog_factory_;
}
private:
base::MockCallback<PasswordAccessoryController::CreateDialogFactory>
mock_dialog_factory_;
};
TEST_F(PasswordAccessoryControllerTest, IsNotRecreatedForSameWebContents) {
PasswordAccessoryController* initial_controller =
PasswordAccessoryController::FromWebContents(web_contents());
EXPECT_NE(nullptr, initial_controller);
PasswordAccessoryController::CreateForWebContents(web_contents());
EXPECT_EQ(PasswordAccessoryController::FromWebContents(web_contents()),
initial_controller);
}
TEST_F(PasswordAccessoryControllerTest, TransformsMatchesToSuggestions) {
EXPECT_CALL(
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), true,
ItemType::SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable({CreateEntry("Ben", "S3cur3").first},
GURL(kExampleSite));
}
TEST_F(PasswordAccessoryControllerTest, HintsToEmptyUserNames) {
EXPECT_CALL(
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(no_user_str(), no_user_str(), false,
ItemType::NON_INTERACTIVE_SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"),
password_for_str(no_user_str()), true,
ItemType::SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable({CreateEntry("", "S3cur3").first},
GURL(kExampleSite));
}
TEST_F(PasswordAccessoryControllerTest, SortsAlphabeticalDuringTransform) {
EXPECT_CALL(
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(
MatchesLabel(passwords_title_str(kExampleDomain)),
MatchesItem(ASCIIToUTF16("Alf"), ASCIIToUTF16("Alf"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("PWD"), password_for_str("Alf"), true,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("Ben"), ASCIIToUTF16("Ben"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("S3cur3"), password_for_str("Ben"), true,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("Cat"), ASCIIToUTF16("Cat"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("M1@u"), password_for_str("Cat"), true,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("Zebra"), ASCIIToUTF16("Zebra"), false,
ItemType::SUGGESTION),
MatchesItem(ASCIIToUTF16("M3h"), password_for_str("Zebra"), true,
ItemType::SUGGESTION),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable(
{CreateEntry("Ben", "S3cur3").first, CreateEntry("Zebra", "M3h").first,
CreateEntry("Alf", "PWD").first, CreateEntry("Cat", "M1@u").first},
GURL(kExampleSite));
}
TEST_F(PasswordAccessoryControllerTest, ClearsSuggestionsOnFrameNavigation) {
// Set any, non-empty password list.
EXPECT_CALL(*view(), OnItemsAvailable(GURL(kExampleSite), _));
controller()->OnPasswordsAvailable({CreateEntry("Ben", "S3cur3").first},
GURL(kExampleSite));
// Pretend that a navigation happened.
EXPECT_CALL(
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(MatchesLabel(passwords_empty_str(kExampleDomain)),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->DidNavigateMainFrame();
}
TEST_F(PasswordAccessoryControllerTest, ProvidesEmptySuggestionsMessage) {
EXPECT_CALL(
*view(),
OnItemsAvailable(
GURL(kExampleSite),
ElementsAre(MatchesLabel(passwords_empty_str(kExampleDomain)),
IsDivider(), MatchesOption(manage_passwords_str()))));
controller()->OnPasswordsAvailable({}, GURL(kExampleSite));
}
TEST_F(PasswordAccessoryControllerTest, IgnoresCrossOriginCalls) {
// Don't expect any call to |OnItemsAvailable|. (https://crbug.com/854150)
EXPECT_CALL(*view(), OnItemsAvailable(_, _)).Times(0);
controller()->OnPasswordsAvailable({CreateEntry("Ben", "S3cur3").first},
GURL("https://other-domain.com"));
}
TEST_F(PasswordAccessoryControllerTest, RelaysAutomaticGenerationAvailable) {
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(true));
controller()->OnAutomaticGenerationStatusChanged(
true, GetTestGenerationUIData1(), nullptr);
}
TEST_F(PasswordAccessoryControllerTest, RelaysAutmaticGenerationUnavailable) {
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(false));
controller()->OnAutomaticGenerationStatusChanged(false, base::nullopt,
nullptr);
}
// Tests that if AutomaticGenerationStatusChanged(true) is called for different
// password forms, the form and field signatures used for password generation
// are updated.
TEST_F(PasswordAccessoryControllerTest,
UpdatesSignaturesForDifferentGenerationForms) {
MockPasswordManagerDriver mock_driver;
password_manager::StubPasswordManagerClient stub_client;
MockPasswordGenerationManager mock_generation_manager(&stub_client,
&mock_driver);
// Called twice for different forms.
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(true)).Times(2);
controller()->OnAutomaticGenerationStatusChanged(
true, GetTestGenerationUIData1(), (&mock_driver)->AsWeakPtr());
PasswordGenerationUIData new_ui_data = GetTestGenerationUIData2();
controller()->OnAutomaticGenerationStatusChanged(true, new_ui_data,
(&mock_driver)->AsWeakPtr());
autofill::FormSignature form_signature =
autofill::CalculateFormSignature(new_ui_data.password_form.form_data);
autofill::FieldSignature field_signature =
autofill::CalculateFieldSignatureByNameAndType(
new_ui_data.generation_element, "password");
std::unique_ptr<MockPasswordGenerationDialogView> dialog_view =
std::make_unique<MockPasswordGenerationDialogView>();
MockPasswordGenerationDialogView* raw_dialog_view = dialog_view.get();
base::string16 generated_password = ASCIIToUTF16("t3stp@ssw0rd");
EXPECT_CALL(mock_dialog_factory(), Run)
.WillOnce(Return(ByMove(std::move(dialog_view))));
EXPECT_CALL(mock_driver, GetPasswordGenerationManager())
.WillOnce(Return(&mock_generation_manager));
EXPECT_CALL(mock_generation_manager,
GeneratePassword(_, form_signature, field_signature,
uint32_t(new_ui_data.max_length), _))
.WillOnce(Return(generated_password));
EXPECT_CALL(*raw_dialog_view, Show(generated_password));
controller()->OnGenerationRequested();
}