blob: 9c6f762547339895586477933dcffc848d8f9206 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/webid/fedcm_account_selection_view_desktop.h"
#include <memory>
#include <string>
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/ui/views/webid/account_selection_bubble_view.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "content/public/browser/identity_request_dialog_controller.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/test/mock_input_event_activation_protector.h"
#include "url/gurl.h"
using LoginState = content::IdentityRequestAccount::LoginState;
using SignInMode = content::IdentityRequestAccount::SignInMode;
namespace {
constexpr char kTopFrameEtldPlusOne[] = "top-frame-example.com";
constexpr char kIframeEtldPlusOne[] = "iframe-example.com";
constexpr char kIdpEtldPlusOne[] = "idp-example.com";
// Mock AccountSelectionBubbleViewInterface which tracks state.
class TestBubbleView : public AccountSelectionBubbleViewInterface {
public:
enum class SheetType {
kAccountPicker,
kConfirmAccount,
kVerifying,
kFailure
};
TestBubbleView() = default;
~TestBubbleView() override = default;
TestBubbleView(const TestBubbleView&) = delete;
TestBubbleView& operator=(const TestBubbleView&) = delete;
void ShowMultiAccountPicker(
const std::vector<IdentityProviderDisplayData>& idp_data_list) override {
show_back_button_ = false;
sheet_type_ = SheetType::kAccountPicker;
account_ids_.clear();
for (content::IdentityRequestAccount account : idp_data_list[0].accounts) {
account_ids_.push_back(account.id);
}
}
void ShowVerifyingSheet(const content::IdentityRequestAccount& account,
const IdentityProviderDisplayData& idp_data,
const std::u16string& title) override {
sheet_type_ = SheetType::kVerifying;
account_ids_ = {account.id};
}
void ShowSingleAccountConfirmDialog(
const std::u16string& top_frame_for_display,
const absl::optional<std::u16string>& iframe_for_display,
const content::IdentityRequestAccount& account,
const IdentityProviderDisplayData& idp_data,
bool show_back_button) override {
show_back_button_ = show_back_button;
sheet_type_ = SheetType::kConfirmAccount;
account_ids_ = {account.id};
}
void ShowFailureDialog(
const std::u16string& top_frame_for_display,
const std::u16string& idp_for_display,
const content::IdentityProviderMetadata& idp_metadata) override {
sheet_type_ = SheetType::kFailure;
account_ids_ = {};
}
std::string GetDialogTitle() const override { return std::string(); }
absl::optional<std::string> GetDialogSubtitle() const override {
return absl::nullopt;
}
bool show_back_button_{false};
absl::optional<SheetType> sheet_type_;
std::vector<std::string> account_ids_;
};
// Test FedCmAccountSelectionView which uses TestBubbleView.
class TestFedCmAccountSelectionView : public FedCmAccountSelectionView {
public:
TestFedCmAccountSelectionView(Delegate* delegate,
views::Widget* widget,
TestBubbleView* bubble_view)
: FedCmAccountSelectionView(delegate),
widget_(widget),
bubble_view_(bubble_view) {
auto input_protector =
std::make_unique<views::MockInputEventActivationProtector>();
ON_CALL(*input_protector, IsPossiblyUnintendedInteraction)
.WillByDefault(testing::Return(false));
SetInputEventActivationProtectorForTesting(std::move(input_protector));
}
TestFedCmAccountSelectionView(const TestFedCmAccountSelectionView&) = delete;
TestFedCmAccountSelectionView& operator=(
const TestFedCmAccountSelectionView&) = delete;
size_t num_bubbles_{0u};
protected:
views::Widget* CreateBubbleWithAccessibleTitle(
const std::u16string& top_frame_etld_plus_one,
const absl::optional<std::u16string>& iframe_etld_plus_one,
const absl::optional<std::u16string>& idp_title,
blink::mojom::RpContext rp_context,
bool show_auto_reauthn_checkbox) override {
++num_bubbles_;
return widget_;
}
AccountSelectionBubbleViewInterface* GetBubbleView() override {
return bubble_view_;
}
private:
base::raw_ptr<views::Widget> widget_;
base::raw_ptr<TestBubbleView> bubble_view_;
};
// Stub AccountSelectionView::Delegate.
class StubAccountSelectionViewDelegate : public AccountSelectionView::Delegate {
public:
explicit StubAccountSelectionViewDelegate(content::WebContents* web_contents)
: web_contents_(web_contents) {}
~StubAccountSelectionViewDelegate() override = default;
StubAccountSelectionViewDelegate(const StubAccountSelectionViewDelegate&) =
delete;
StubAccountSelectionViewDelegate& operator=(
const StubAccountSelectionViewDelegate&) = delete;
void OnAccountSelected(const GURL&,
const content::IdentityRequestAccount&) override {}
void OnDismiss(
content::IdentityRequestDialogController::DismissReason) override {}
gfx::NativeView GetNativeView() override { return gfx::NativeView(); }
content::WebContents* GetWebContents() override { return web_contents_; }
private:
base::raw_ptr<content::WebContents> web_contents_;
};
} // namespace
class FedCmAccountSelectionViewDesktopTest : public ChromeViewsTestBase {
public:
void SetUp() override {
ChromeViewsTestBase::SetUp();
test_web_contents_ =
content::WebContentsTester::CreateTestWebContents(&profile_, nullptr);
delegate_ = std::make_unique<StubAccountSelectionViewDelegate>(
test_web_contents_.get());
widget_.reset(CreateTestWidget().release());
bubble_view_ = std::make_unique<TestBubbleView>();
}
IdentityProviderDisplayData CreateIdentityProviderDisplayData(
const std::vector<std::pair<std::string, LoginState>>& account_infos) {
std::vector<content::IdentityRequestAccount> accounts;
for (const auto& account_info : account_infos) {
accounts.emplace_back(account_info.first, "", "", "", GURL::EmptyGURL(),
account_info.second);
}
return IdentityProviderDisplayData(u"", content::IdentityProviderMetadata(),
content::ClientMetadata(GURL(), GURL()),
std::move(accounts));
}
std::unique_ptr<TestFedCmAccountSelectionView> CreateAndShow(
const std::vector<content::IdentityRequestAccount>& accounts,
SignInMode mode,
bool show_auto_reauthn_checkbox = false) {
auto controller = std::make_unique<TestFedCmAccountSelectionView>(
delegate_.get(), widget_.get(), bubble_view_.get());
Show(*controller, accounts, mode);
return controller;
}
void Show(TestFedCmAccountSelectionView& controller,
const std::vector<content::IdentityRequestAccount>& accounts,
SignInMode mode,
bool show_auto_reauthn_checkbox = false) {
controller.Show(
kTopFrameEtldPlusOne,
absl::make_optional<std::string>(kIframeEtldPlusOne),
{{kIdpEtldPlusOne, accounts, content::IdentityProviderMetadata(),
content::ClientMetadata(GURL(), GURL()),
blink::mojom::RpContext::kSignIn}},
mode, show_auto_reauthn_checkbox);
}
ui::MouseEvent CreateMouseEvent() {
return ui::MouseEvent(ui::ET_MOUSE_PRESSED, gfx::Point(), gfx::Point(),
base::TimeTicks(), ui::EF_LEFT_MOUSE_BUTTON, 0);
}
protected:
TestingProfile profile_;
// This enables uses of TestWebContents.
content::RenderViewHostTestEnabler test_render_host_factories_;
std::unique_ptr<content::WebContents> test_web_contents_;
views::ViewsTestBase::WidgetAutoclosePtr widget_;
std::unique_ptr<TestBubbleView> bubble_view_;
std::unique_ptr<AccountSelectionView::Delegate> delegate_;
base::HistogramTester histogram_tester_;
};
TEST_F(FedCmAccountSelectionViewDesktopTest, SingleAccountFlow) {
const char kAccountId[] = "account_id";
IdentityProviderDisplayData idp_data =
CreateIdentityProviderDisplayData({{kAccountId, LoginState::kSignUp}});
const std::vector<Account>& accounts = idp_data.accounts;
std::unique_ptr<TestFedCmAccountSelectionView> controller =
CreateAndShow(accounts, SignInMode::kExplicit);
AccountSelectionBubbleView::Observer* observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
EXPECT_FALSE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kConfirmAccount,
bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId));
observer->OnAccountSelected(accounts[0], idp_data, CreateMouseEvent());
EXPECT_EQ(TestBubbleView::SheetType::kVerifying, bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId));
}
TEST_F(FedCmAccountSelectionViewDesktopTest, MultipleAccountFlowReturning) {
const char kAccountId1[] = "account_id1";
const char kAccountId2[] = "account_id2";
IdentityProviderDisplayData idp_data = CreateIdentityProviderDisplayData(
{{kAccountId1, LoginState::kSignIn}, {kAccountId2, LoginState::kSignIn}});
const std::vector<Account>& accounts = idp_data.accounts;
std::unique_ptr<TestFedCmAccountSelectionView> controller =
CreateAndShow(accounts, SignInMode::kExplicit);
AccountSelectionBubbleView::Observer* observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
EXPECT_FALSE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kAccountPicker,
bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_,
testing::ElementsAre(kAccountId1, kAccountId2));
observer->OnAccountSelected(accounts[0], idp_data, CreateMouseEvent());
EXPECT_EQ(TestBubbleView::SheetType::kVerifying, bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId1));
}
TEST_F(FedCmAccountSelectionViewDesktopTest, MultipleAccountFlowBack) {
const char kAccountId1[] = "account_id1";
const char kAccountId2[] = "account_id2";
IdentityProviderDisplayData idp_data = CreateIdentityProviderDisplayData({
{kAccountId1, LoginState::kSignUp},
{kAccountId2, LoginState::kSignUp},
});
const std::vector<Account>& accounts = idp_data.accounts;
std::unique_ptr<TestFedCmAccountSelectionView> controller =
CreateAndShow(accounts, SignInMode::kExplicit);
AccountSelectionBubbleView::Observer* observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
EXPECT_FALSE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kAccountPicker,
bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_,
testing::ElementsAre(kAccountId1, kAccountId2));
observer->OnAccountSelected(accounts[0], idp_data, CreateMouseEvent());
EXPECT_TRUE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kConfirmAccount,
bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId1));
observer->OnBackButtonClicked();
EXPECT_FALSE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kAccountPicker,
bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_,
testing::ElementsAre(kAccountId1, kAccountId2));
observer->OnAccountSelected(accounts[1], idp_data, CreateMouseEvent());
EXPECT_TRUE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kConfirmAccount,
bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId2));
observer->OnAccountSelected(accounts[1], idp_data, CreateMouseEvent());
EXPECT_EQ(TestBubbleView::SheetType::kVerifying, bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId2));
}
// Test transitioning from IdP sign-in status mismatch failure dialog to regular
// sign-in dialog.
TEST_F(FedCmAccountSelectionViewDesktopTest,
IdpSigninStatusMismatchDialogToSigninFlow) {
auto controller = std::make_unique<TestFedCmAccountSelectionView>(
delegate_.get(), widget_.get(), bubble_view_.get());
AccountSelectionBubbleView::Observer* observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
controller->ShowFailureDialog(kTopFrameEtldPlusOne, kIdpEtldPlusOne,
content::IdentityProviderMetadata());
EXPECT_EQ(TestBubbleView::SheetType::kFailure, bubble_view_->sheet_type_);
const char kAccountId[] = "account_id";
IdentityProviderDisplayData idp_data = CreateIdentityProviderDisplayData({
{kAccountId, LoginState::kSignUp},
});
Show(*controller, idp_data.accounts, SignInMode::kExplicit);
EXPECT_EQ(TestBubbleView::SheetType::kConfirmAccount,
bubble_view_->sheet_type_);
observer->OnAccountSelected(idp_data.accounts[0], idp_data,
CreateMouseEvent());
EXPECT_EQ(TestBubbleView::SheetType::kVerifying, bubble_view_->sheet_type_);
// Failure bubble should have been re-used for sign-in dialog.
EXPECT_EQ(1u, controller->num_bubbles_);
}
// Test transitioning from IdP sign-in status mismatch failure dialog to regular
// sign-in dialog while the dialog is hidden. This emulates a user signing
// into the IdP in a different tab.
TEST_F(FedCmAccountSelectionViewDesktopTest,
IdpSigninStatusMismatchDialogToSigninFlowHidden) {
auto controller = std::make_unique<TestFedCmAccountSelectionView>(
delegate_.get(), widget_.get(), bubble_view_.get());
AccountSelectionBubbleView::Observer* observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
controller->ShowFailureDialog(kTopFrameEtldPlusOne, kIdpEtldPlusOne,
content::IdentityProviderMetadata());
EXPECT_EQ(TestBubbleView::SheetType::kFailure, bubble_view_->sheet_type_);
const char kAccountId[] = "account_id";
IdentityProviderDisplayData idp_data = CreateIdentityProviderDisplayData({
{kAccountId, LoginState::kSignUp},
});
// If the user switched tabs to sign-into the IdP, Show() may be called while
// the associated FedCM tab is inactive. Show() should not show the
// views::Widget in this case.
controller->OnVisibilityChanged(content::Visibility::HIDDEN);
Show(*controller, idp_data.accounts, SignInMode::kExplicit);
EXPECT_FALSE(widget_->IsVisible());
controller->OnVisibilityChanged(content::Visibility::VISIBLE);
EXPECT_TRUE(widget_->IsVisible());
EXPECT_EQ(TestBubbleView::SheetType::kConfirmAccount,
bubble_view_->sheet_type_);
observer->OnAccountSelected(idp_data.accounts[0], idp_data,
CreateMouseEvent());
EXPECT_EQ(TestBubbleView::SheetType::kVerifying, bubble_view_->sheet_type_);
// Failure bubble should have been re-used for sign-in dialog.
EXPECT_EQ(1u, controller->num_bubbles_);
}
TEST_F(FedCmAccountSelectionViewDesktopTest, AutoReauthnSingleAccountFlow) {
const char kAccountId[] = "account_id";
IdentityProviderDisplayData idp_data =
CreateIdentityProviderDisplayData({{kAccountId, LoginState::kSignIn}});
const std::vector<Account>& accounts = idp_data.accounts;
std::unique_ptr<TestFedCmAccountSelectionView> controller =
CreateAndShow(accounts, SignInMode::kAuto);
EXPECT_FALSE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kVerifying, bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId));
}
namespace {
// AccountSelectionViewDelegate which deletes the FedCmAccountSelectionView in
// OnAccountSelected().
class ViewDeletingAccountSelectionViewDelegate
: public StubAccountSelectionViewDelegate {
public:
explicit ViewDeletingAccountSelectionViewDelegate(
content::WebContents* web_contents)
: StubAccountSelectionViewDelegate(web_contents) {}
~ViewDeletingAccountSelectionViewDelegate() override = default;
ViewDeletingAccountSelectionViewDelegate(
const ViewDeletingAccountSelectionViewDelegate&) = delete;
ViewDeletingAccountSelectionViewDelegate& operator=(
const ViewDeletingAccountSelectionViewDelegate&) = delete;
void SetView(std::unique_ptr<FedCmAccountSelectionView> view) {
view_ = std::move(view);
}
void OnAccountSelected(const GURL&,
const content::IdentityRequestAccount&) override {
view_.reset();
}
private:
std::unique_ptr<FedCmAccountSelectionView> view_;
};
} // namespace
TEST_F(FedCmAccountSelectionViewDesktopTest, AccountSelectedDeletesView) {
delegate_ = std::make_unique<ViewDeletingAccountSelectionViewDelegate>(
test_web_contents_.get());
ViewDeletingAccountSelectionViewDelegate* view_deleting_delegate =
static_cast<ViewDeletingAccountSelectionViewDelegate*>(delegate_.get());
const char kAccountId1[] = "account_id1";
IdentityProviderDisplayData idp_data = CreateIdentityProviderDisplayData({
{kAccountId1, LoginState::kSignIn},
});
const std::vector<Account>& accounts = idp_data.accounts;
AccountSelectionBubbleView::Observer* observer = nullptr;
{
std::unique_ptr<TestFedCmAccountSelectionView> controller =
CreateAndShow(accounts, SignInMode::kExplicit);
observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
view_deleting_delegate->SetView(std::move(controller));
}
// Destroys FedCmAccountSelectionView. Should not cause crash.
observer->OnAccountSelected(accounts[0], idp_data, CreateMouseEvent());
}
TEST_F(FedCmAccountSelectionViewDesktopTest, ClickProtection) {
const char kAccountId[] = "account_id";
IdentityProviderDisplayData idp_data =
CreateIdentityProviderDisplayData({{kAccountId, LoginState::kSignUp}});
const std::vector<Account>& accounts = idp_data.accounts;
std::unique_ptr<TestFedCmAccountSelectionView> controller =
CreateAndShow(accounts, SignInMode::kExplicit);
AccountSelectionBubbleView::Observer* observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
// Use a mock input protector to more easily test. The protector rejects the
// first input and accepts any subsequent input.
auto input_protector =
std::make_unique<views::MockInputEventActivationProtector>();
EXPECT_CALL(*input_protector, IsPossiblyUnintendedInteraction)
.WillOnce(testing::Return(true))
.WillRepeatedly(testing::Return(false));
controller->SetInputEventActivationProtectorForTesting(
std::move(input_protector));
observer->OnAccountSelected(accounts[0], idp_data, CreateMouseEvent());
// Nothing should change after first account selected.
EXPECT_FALSE(bubble_view_->show_back_button_);
EXPECT_EQ(TestBubbleView::SheetType::kConfirmAccount,
bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId));
observer->OnAccountSelected(accounts[0], idp_data, CreateMouseEvent());
// Should show verifying sheet after first account selected.
EXPECT_EQ(TestBubbleView::SheetType::kVerifying, bubble_view_->sheet_type_);
EXPECT_THAT(bubble_view_->account_ids_, testing::ElementsAre(kAccountId));
}
// Tests that when the auth re-authn dialog is closed, the relevant metric is
// recorded.
TEST_F(FedCmAccountSelectionViewDesktopTest, CloseAutoReauthnSheetMetric) {
const char kAccountId[] = "account_id";
IdentityProviderDisplayData idp_data =
CreateIdentityProviderDisplayData({{kAccountId, LoginState::kSignIn}});
const std::vector<Account>& accounts = idp_data.accounts;
std::unique_ptr<TestFedCmAccountSelectionView> controller =
CreateAndShow(accounts, SignInMode::kAuto);
histogram_tester_.ExpectTotalCount("Blink.FedCm.ClosedSheetType.Desktop", 0);
AccountSelectionBubbleView::Observer* observer =
static_cast<AccountSelectionBubbleView::Observer*>(controller.get());
observer->OnCloseButtonClicked(CreateMouseEvent());
histogram_tester_.ExpectUniqueSample(
"Blink.FedCm.ClosedSheetType.Desktop",
static_cast<int>(FedCmAccountSelectionView::SheetType::AUTO_REAUTHN), 1);
}