| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "base/command_line.h" |
| #include "base/functional/bind.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "chrome/browser/ash/login/test/device_state_mixin.h" |
| #include "chrome/browser/ash/notifications/echo_dialog_view.h" |
| #include "chrome/browser/chromeos/extensions/echo_private/echo_private_api.h" |
| #include "chrome/browser/chromeos/extensions/echo_private/echo_private_api_util.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/window_controller.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/test/base/mixin_based_in_process_browser_test.h" |
| #include "chromeos/ash/components/policy/device_policy/cached_device_policy_updater.h" |
| #include "chromeos/ash/components/settings/cros_settings.h" |
| #include "chromeos/ash/components/settings/cros_settings_names.h" |
| #include "chromeos/ash/components/settings/cros_settings_waiter.h" |
| #include "chromeos/ash/components/system/fake_statistics_provider.h" |
| #include "chromeos/ash/components/system/statistics_provider.h" |
| #include "content/public/test/browser_test.h" |
| #include "extensions/browser/api_test_utils.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/mojom/dialog_button.mojom.h" |
| |
| namespace utils = extensions::api_test_utils; |
| |
| namespace chromeos { |
| |
| class ExtensionEchoPrivateApiTest : public InProcessBrowserTestMixinHostSupport< |
| extensions::ExtensionApiTest> { |
| public: |
| enum DialogTestAction { |
| DIALOG_TEST_ACTION_NONE, |
| DIALOG_TEST_ACTION_ACCEPT, |
| DIALOG_TEST_ACTION_CANCEL, |
| }; |
| |
| ExtensionEchoPrivateApiTest() |
| : expected_dialog_buttons_( |
| static_cast<int>(ui::mojom::DialogButton::kNone)), |
| dialog_action_(DIALOG_TEST_ACTION_NONE), |
| dialog_invocation_count_(0) {} |
| |
| ~ExtensionEchoPrivateApiTest() override = default; |
| |
| void SetUp() override { |
| statistics_provider_.SetVpdStatus( |
| ash::system::StatisticsProvider::VpdStatus::kValid); |
| statistics_provider_.SetMachineStatistic(ash::system::kOffersCouponCodeKey, |
| "COUPON_CODE"); |
| statistics_provider_.SetMachineStatistic(ash::system::kOffersGroupCodeKey, |
| "GROUP_CODE"); |
| statistics_provider_.SetMachineStatistic(ash::system::kActivateDateKey, |
| "2024-13"); |
| ash::system::StatisticsProvider::SetTestProvider(&statistics_provider_); |
| |
| ash::EchoDialogView::AddShowCallbackForTesting(base::BindOnce( |
| &ExtensionEchoPrivateApiTest::OnDialogShown, base::Unretained(this))); |
| extensions::ExtensionApiTest::SetUp(); |
| } |
| |
| void RunDefaultGetUserFunctionAndExpectResultEquals(int tab_id, |
| bool expected_result) { |
| auto function = base::MakeRefCounted<EchoPrivateGetUserConsentFunction>(); |
| function->set_has_callback(true); |
| |
| const std::string arguments = base::StringPrintf( |
| R"([{"serviceName": "name", "origin": "https://test.com", "tabId": %d}])", |
| tab_id); |
| std::optional<base::Value> result = utils::RunFunctionAndReturnSingleResult( |
| function.get(), arguments, profile()); |
| |
| ASSERT_TRUE(result); |
| ASSERT_EQ(base::Value::Type::BOOLEAN, result->type()); |
| |
| EXPECT_EQ(expected_result, result->GetBool()); |
| } |
| |
| void OnDialogShown(ash::EchoDialogView* dialog) { |
| dialog_invocation_count_++; |
| ASSERT_LE(dialog_invocation_count_, 1); |
| |
| EXPECT_EQ(expected_dialog_buttons_, dialog->buttons()); |
| |
| // Don't accept the dialog if the dialog buttons don't match expectation. |
| // Accepting a dialog which should not have accept option may crash the |
| // test. The test already failed, so it's ok to cancel the dialog. |
| DialogTestAction dialog_action = dialog_action_; |
| if (dialog_action == DIALOG_TEST_ACTION_ACCEPT && |
| expected_dialog_buttons_ != dialog->buttons()) { |
| dialog_action = DIALOG_TEST_ACTION_CANCEL; |
| } |
| |
| // Perform test action on the dialog. |
| // The dialog should stay around until AcceptWindow or CancelWindow is |
| // called, so base::Unretained is safe. |
| if (dialog_action == DIALOG_TEST_ACTION_ACCEPT) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(base::IgnoreResult(&ash::EchoDialogView::Accept), |
| base::Unretained(dialog))); |
| } else if (dialog_action == DIALOG_TEST_ACTION_CANCEL) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(base::IgnoreResult(&ash::EchoDialogView::Cancel), |
| base::Unretained(dialog))); |
| } |
| } |
| |
| int dialog_invocation_count() const { |
| return dialog_invocation_count_; |
| } |
| |
| // Open and activates tab in the test browser. Returns the ID of the opened |
| // tab. |
| int OpenAndActivateTab() { |
| EXPECT_TRUE( |
| AddTabAtIndex(0, GURL("about:blank"), ui::PAGE_TRANSITION_LINK)); |
| browser()->tab_strip_model()->ActivateTabAt( |
| 0, TabStripUserGestureDetails( |
| TabStripUserGestureDetails::GestureType::kOther)); |
| return extensions::ExtensionTabUtil::GetTabId( |
| browser()->tab_strip_model()->GetActiveWebContents()); |
| } |
| |
| bool CloseTabWithId(int tab_id) { |
| extensions::WindowController* window = nullptr; |
| int tab_index = -1; |
| if (!extensions::ExtensionTabUtil::GetTabById( |
| tab_id, profile(), false, &window, nullptr, &tab_index) || |
| !window) { |
| ADD_FAILURE() << "Tab not found " << tab_id; |
| return false; |
| } |
| |
| TabStripModel* tab_strip = window->GetBrowser()->tab_strip_model(); |
| int previous_tab_count = tab_strip->count(); |
| tab_strip->CloseWebContentsAt(tab_index, 0); |
| return (previous_tab_count - 1) == tab_strip->count(); |
| } |
| |
| void EnsureAllowRedeemOffers(bool expected) { |
| bool value = false; |
| if (ash::CrosSettings::Get()->GetBoolean( |
| ash::kAllowRedeemChromeOsRegistrationOffers, &value) && |
| value == expected) { |
| return; |
| } |
| ash::CrosSettingsWaiter waiter( |
| {ash::kAllowRedeemChromeOsRegistrationOffers}); |
| policy::CachedDevicePolicyUpdater updater; |
| updater.payload().mutable_allow_redeem_offers()->set_allow_redeem_offers( |
| expected); |
| updater.Commit(); |
| waiter.Wait(); |
| } |
| |
| protected: |
| int expected_dialog_buttons_; |
| DialogTestAction dialog_action_; |
| ash::system::FakeStatisticsProvider statistics_provider_; |
| |
| private: |
| ash::DeviceStateMixin device_state_{ |
| &mixin_host_, |
| ash::DeviceStateMixin::State::OOBE_COMPLETED_CONSUMER_OWNED}; |
| int dialog_invocation_count_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, EchoTest) { |
| EXPECT_TRUE(RunExtensionTest("echo/component_extension", {}, |
| {.load_as_component = true})) |
| << message_; |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, |
| GetUserConsent_InvalidOrigin) { |
| const int tab_id = OpenAndActivateTab(); |
| |
| expected_dialog_buttons_ = static_cast<int>(ui::mojom::DialogButton::kNone); |
| dialog_action_ = DIALOG_TEST_ACTION_NONE; |
| |
| auto function = base::MakeRefCounted<EchoPrivateGetUserConsentFunction>(); |
| |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), |
| base::StringPrintf( |
| R"([{"serviceName": "name", "origin": "invalid", "tabId": %d}])", |
| tab_id), |
| profile()); |
| |
| EXPECT_EQ("Invalid origin.", error); |
| EXPECT_EQ(0, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, GetUserConsent_NoTabIdSet) { |
| expected_dialog_buttons_ = static_cast<int>(ui::mojom::DialogButton::kNone); |
| dialog_action_ = DIALOG_TEST_ACTION_NONE; |
| |
| auto function = base::MakeRefCounted<EchoPrivateGetUserConsentFunction>(); |
| |
| std::string error = utils::RunFunctionAndReturnError( |
| function.get(), |
| R"([{"serviceName": "name", "origin": "https://test.com"}])", profile()); |
| |
| EXPECT_EQ("Not called from an app window - the tabId is required.", error); |
| EXPECT_EQ(0, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, |
| GetUserConsent_InactiveTab) { |
| const int tab_id = OpenAndActivateTab(); |
| // Open and activate another tab. |
| OpenAndActivateTab(); |
| |
| expected_dialog_buttons_ = static_cast<int>(ui::mojom::DialogButton::kNone); |
| dialog_action_ = DIALOG_TEST_ACTION_NONE; |
| |
| auto function = base::MakeRefCounted<EchoPrivateGetUserConsentFunction>(); |
| |
| const std::string arguments = base::StringPrintf( |
| R"([{"serviceName": "name", "origin": "https://test.com", "tabId": %d}])", |
| tab_id); |
| std::string error = |
| utils::RunFunctionAndReturnError(function.get(), arguments, profile()); |
| |
| EXPECT_EQ("Consent requested from an inactive tab.", error); |
| EXPECT_EQ(0, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, GetUserConsent_ClosedTab) { |
| const int tab_id = OpenAndActivateTab(); |
| ASSERT_TRUE(CloseTabWithId(tab_id)); |
| |
| expected_dialog_buttons_ = static_cast<int>(ui::mojom::DialogButton::kNone); |
| dialog_action_ = DIALOG_TEST_ACTION_NONE; |
| |
| auto function = base::MakeRefCounted<EchoPrivateGetUserConsentFunction>(); |
| |
| const std::string arguments = base::StringPrintf( |
| R"([{"serviceName": "name", "origin": "https://test.com", "tabId": %d}])", |
| tab_id); |
| std::string error = |
| utils::RunFunctionAndReturnError(function.get(), arguments, profile()); |
| |
| EXPECT_EQ("Tab not found.", error); |
| EXPECT_EQ(0, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, |
| GetUserConsent_AllowRedeemPrefNotSet) { |
| const int tab_id = OpenAndActivateTab(); |
| expected_dialog_buttons_ = |
| static_cast<int>(ui::mojom::DialogButton::kCancel) | |
| static_cast<int>(ui::mojom::DialogButton::kOk); |
| dialog_action_ = DIALOG_TEST_ACTION_ACCEPT; |
| |
| RunDefaultGetUserFunctionAndExpectResultEquals(tab_id, true); |
| |
| EXPECT_EQ(1, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, |
| GetUserConsent_AllowRedeemPrefTrue) { |
| const int tab_id = OpenAndActivateTab(); |
| |
| EnsureAllowRedeemOffers(true); |
| |
| expected_dialog_buttons_ = |
| static_cast<int>(ui::mojom::DialogButton::kCancel) | |
| static_cast<int>(ui::mojom::DialogButton::kOk); |
| dialog_action_ = DIALOG_TEST_ACTION_ACCEPT; |
| |
| RunDefaultGetUserFunctionAndExpectResultEquals(tab_id, true); |
| |
| EXPECT_EQ(1, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, |
| GetUserConsent_ConsentDenied) { |
| const int tab_id = OpenAndActivateTab(); |
| |
| EnsureAllowRedeemOffers(true); |
| |
| expected_dialog_buttons_ = |
| static_cast<int>(ui::mojom::DialogButton::kCancel) | |
| static_cast<int>(ui::mojom::DialogButton::kOk); |
| dialog_action_ = DIALOG_TEST_ACTION_CANCEL; |
| |
| RunDefaultGetUserFunctionAndExpectResultEquals(tab_id, false); |
| |
| EXPECT_EQ(1, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, |
| GetUserConsent_AllowRedeemPrefFalse) { |
| const int tab_id = OpenAndActivateTab(); |
| |
| EnsureAllowRedeemOffers(false); |
| |
| expected_dialog_buttons_ = static_cast<int>(ui::mojom::DialogButton::kCancel); |
| dialog_action_ = DIALOG_TEST_ACTION_CANCEL; |
| |
| RunDefaultGetUserFunctionAndExpectResultEquals(tab_id, false); |
| |
| EXPECT_EQ(1, dialog_invocation_count()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(ExtensionEchoPrivateApiTest, RemoveEmptyValueDicts) { |
| auto dict = base::Value::Dict() |
| .Set("a", "b") |
| .Set("empty", base::Value::Dict()) |
| .Set("nested", base::Value::Dict().Set("c", "d").Set( |
| "empty_value", base::Value::Dict())) |
| .Set("nested_empty", base::Value::Dict().Set( |
| "empty_value", base::Value::Dict())); |
| |
| // Remove nested dictionaries. |
| chromeos::echo_offer::RemoveEmptyValueDicts(dict); |
| |
| // After removing empty nested dicts, we are left with: |
| // {"a" : "b", "nested" : {"c" : "d"}} |
| EXPECT_EQ(2u, dict.size()); |
| EXPECT_EQ(1u, dict.FindDict("nested")->size()); |
| EXPECT_EQ("b", *dict.FindString("a")); |
| EXPECT_EQ("d", *dict.FindStringByDottedPath("nested.c")); |
| } |
| |
| } // namespace chromeos |