blob: cf6c99a1d20aa90af110aac431cb12dfe5a592c8 [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 <memory>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/task/single_thread_task_runner.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/prefs.mojom-test-utils.h"
#include "chromeos/crosapi/mojom/prefs.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "chromeos/lacros/lacros_test_helper.h"
#include "chromeos/startup/browser_params_proxy.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/notification_service.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote_set.h"
#include "testing/gtest/include/gtest/gtest.h"
using ContextType = extensions::ExtensionBrowserTest::ContextType;
// Tests for extension-controlled prefs, where an extension in lacros sets a
// pref where the underlying feature lives in ash.
class ExtensionPreferenceApiLacrosBrowserTest
: public extensions::ExtensionApiTest,
public testing::WithParamInterface<ContextType> {
public:
ExtensionPreferenceApiLacrosBrowserTest(
const ExtensionPreferenceApiLacrosBrowserTest&) = delete;
ExtensionPreferenceApiLacrosBrowserTest& operator=(
const ExtensionPreferenceApiLacrosBrowserTest&) = delete;
protected:
ExtensionPreferenceApiLacrosBrowserTest() : ExtensionApiTest(GetParam()) {}
~ExtensionPreferenceApiLacrosBrowserTest() override = default;
void CheckPreferencesSet() {
PrefService* prefs = profile_->GetPrefs();
// From the lacros perspective, the pref should look extension controlled.
const PrefService::Preference* pref =
prefs->FindPreference(prefs::kLacrosAccessibilityAutoclickEnabled);
ASSERT_TRUE(pref);
EXPECT_TRUE(pref->IsExtensionControlled());
EXPECT_TRUE(prefs->GetBoolean(prefs::kLacrosAccessibilityAutoclickEnabled));
}
void CheckPreferencesCleared() {
PrefService* prefs = profile_->GetPrefs();
const PrefService::Preference* pref =
prefs->FindPreference(prefs::kLacrosAccessibilityAutoclickEnabled);
ASSERT_TRUE(pref);
EXPECT_FALSE(pref->IsExtensionControlled());
EXPECT_FALSE(
prefs->GetBoolean(prefs::kLacrosAccessibilityAutoclickEnabled));
}
void SetUp() override {
// When the test changes the value of
// chrome.accessibilityFeatures.autoclick in Ash, the pref value change is
// observed by AccessibilityController and will trigger popping up a dialog
// in Ash with the prompt about confirmation of disabling autoclick. The
// dialog is not closed when the test is torn down in Lacros, and will
// affect other tests running after it if the test runs with shared Ash.
// Therefore, we start a unique Ash to run with this test suite to avoid
// the test isolation issue.
StartUniqueAshChrome(
{}, {}, {},
"crbug.com/1435317 Switch to shared ash when autoclick disable "
"confirmation dialog issue is fixed");
ExtensionApiTest::SetUp();
}
void SetUpOnMainThread() override {
if (!IsServiceAvailable()) {
GTEST_SKIP() << "The Lacros service is not available.";
}
extensions::ExtensionApiTest::SetUpOnMainThread();
// The browser might get closed later (and therefore be destroyed), so we
// save the profile.
profile_ = browser()->profile();
// Closing the last browser window also releases a module reference. Make
// sure it's not the last one, so the message loop doesn't quit
// unexpectedly.
keep_alive_ = std::make_unique<ScopedKeepAlive>(
KeepAliveOrigin::BROWSER, KeepAliveRestartOption::DISABLED);
}
void TearDownOnMainThread() override {
// BrowserProcess::Shutdown() needs to be called in a message loop, so we
// post a task to release the keep alive, then run the message loop.
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&std::unique_ptr<ScopedKeepAlive>::reset,
base::Unretained(&keep_alive_), nullptr));
content::RunAllTasksUntilIdle();
extensions::ExtensionApiTest::TearDownOnMainThread();
}
bool IsServiceAvailable() {
if (chromeos::LacrosService::Get()
->GetInterfaceVersion<crosapi::mojom::Prefs>() <
static_cast<int>(crosapi::mojom::Prefs::MethodMinVersions::
kGetExtensionPrefWithControlMinVersion)) {
LOG(WARNING) << "Unsupported ash version.";
return false;
}
return true;
}
bool DoesAshSupportObservers() {
// Versions of ash without this capability cannot create observers for prefs
// writing to the ash standalone browser prefstore.
constexpr char kExtensionControlledPrefObserversCapability[] =
"crbug/1334964";
return chromeos::BrowserParamsProxy::Get()->AshCapabilities().has_value() &&
base::Contains(
chromeos::BrowserParamsProxy::Get()->AshCapabilities().value(),
kExtensionControlledPrefObserversCapability);
}
raw_ptr<Profile, DanglingUntriaged> profile_ = nullptr;
std::unique_ptr<ScopedKeepAlive> keep_alive_;
};
INSTANTIATE_TEST_SUITE_P(EventPage,
ExtensionPreferenceApiLacrosBrowserTest,
::testing::Values(ContextType::kEventPage));
INSTANTIATE_TEST_SUITE_P(ServiceWorker,
ExtensionPreferenceApiLacrosBrowserTest,
::testing::Values(ContextType::kServiceWorker));
IN_PROC_BROWSER_TEST_P(ExtensionPreferenceApiLacrosBrowserTest, Lacros) {
absl::optional<::base::Value> out_value;
crosapi::mojom::PrefsAsyncWaiter async_waiter(
chromeos::LacrosService::Get()->GetRemote<crosapi::mojom::Prefs>().get());
// At start, the value in ash should not be set.
async_waiter.GetPref(crosapi::mojom::PrefPath::kAccessibilityAutoclickEnabled,
&out_value);
EXPECT_FALSE(out_value.value().GetBool());
base::FilePath extension_path =
test_data_dir_.AppendASCII("preference/lacros");
{
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
EXPECT_TRUE(LoadExtension(extension_path)) << message_;
EXPECT_TRUE(listener.WaitUntilSatisfied());
// Run the tests.
listener.Reply("run test");
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}
CheckPreferencesSet();
// In ash, the value should now be set.
async_waiter.GetPref(crosapi::mojom::PrefPath::kAccessibilityAutoclickEnabled,
&out_value);
EXPECT_TRUE(out_value.value().GetBool());
// The settings should not be reset when the extension is reloaded.
{
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ReloadExtension(last_loaded_extension_id());
EXPECT_TRUE(listener.WaitUntilSatisfied());
listener.Reply("");
}
CheckPreferencesSet();
// Uninstalling and installing the extension (without running the test that
// calls the extension API) should clear the settings.
extensions::TestExtensionRegistryObserver observer(
extensions::ExtensionRegistry::Get(profile_), last_loaded_extension_id());
UninstallExtension(last_loaded_extension_id());
observer.WaitForExtensionUninstalled();
CheckPreferencesCleared();
if (DoesAshSupportObservers()) {
// When the extension in uninstalled, the pref in lacros should be the
// default value (false). This only works if Ash correctly implements
// extension-controlled pref observers.
async_waiter.GetPref(
crosapi::mojom::PrefPath::kAccessibilityAutoclickEnabled, &out_value);
EXPECT_FALSE(out_value.value().GetBool());
}
{
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
EXPECT_TRUE(LoadExtension(extension_path));
EXPECT_TRUE(listener.WaitUntilSatisfied());
listener.Reply("");
}
CheckPreferencesCleared();
}
IN_PROC_BROWSER_TEST_P(ExtensionPreferenceApiLacrosBrowserTest, OnChange) {
if (!DoesAshSupportObservers()) {
LOG(WARNING) << "Ash does not support observers, skipping the test.";
return;
}
EXPECT_TRUE(RunExtensionTest("preference/onchange_lacros", {},
{.allow_in_incognito = false}))
<< message_;
}
// The extension controlled pref observers are only instantiated when a listener
// is actually attached. The purpose of running this test is to ensure that they
// are instantiated as we use the prefs api to create the observer. See also
// crbug/1334985.
IN_PROC_BROWSER_TEST_P(ExtensionPreferenceApiLacrosBrowserTest,
CreateObservers) {
EXPECT_TRUE(
RunExtensionTest("preference/onchange", {}, {.allow_in_incognito = true}))
<< message_;
}
// An implementation of the `crosapi::mojom::Prefs` mojo service which returns
// null when fetching a pref value. Used for testing the Preference API against
// Ash-Lacros version skew where Ash does not recognize the Lacros extension
// pref. Since it's not possible to extend the PrefPath enum at runtime, this
// class implements the same behaviour like the Ash implementation when it does
// not recognize the pref i.e, sends a null value as a response to
// `GetExtensionPrefWithControl`.
class FakePrefsAshService : public crosapi::mojom::Prefs {
public:
FakePrefsAshService() = default;
FakePrefsAshService(const FakePrefsAshService&) = delete;
FakePrefsAshService& operator=(const FakePrefsAshService&) = delete;
~FakePrefsAshService() override {}
private:
// crosapi::mojom::Prefs:
void GetPref(crosapi::mojom::PrefPath path,
GetPrefCallback callback) override {
std::move(callback).Run(absl::nullopt);
}
void SetPref(crosapi::mojom::PrefPath path,
base::Value value,
SetPrefCallback callback) override {
std::move(callback).Run();
}
void AddObserver(
crosapi::mojom::PrefPath path,
mojo::PendingRemote<crosapi::mojom::PrefObserver> observer) override {
mojo::Remote<crosapi::mojom::PrefObserver> remote(std::move(observer));
observers_.Add(std::move(remote));
}
void GetExtensionPrefWithControl(
crosapi::mojom::PrefPath path,
GetExtensionPrefWithControlCallback callback) override {
// Not a valid prefpath
std::move(callback).Run(absl::nullopt,
crosapi::mojom::PrefControlState::kDefaultUnknown);
}
void ClearExtensionControlledPref(
crosapi::mojom::PrefPath path,
ClearExtensionControlledPrefCallback callback) override {
std::move(callback).Run();
}
mojo::RemoteSet<crosapi::mojom::PrefObserver> observers_;
};
class ExtensionPreferenceApiUnsupportedInAshBrowserTest
: public ExtensionPreferenceApiLacrosBrowserTest {
public:
ExtensionPreferenceApiUnsupportedInAshBrowserTest(
const ExtensionPreferenceApiUnsupportedInAshBrowserTest&) = delete;
ExtensionPreferenceApiUnsupportedInAshBrowserTest& operator=(
const ExtensionPreferenceApiUnsupportedInAshBrowserTest&) = delete;
protected:
ExtensionPreferenceApiUnsupportedInAshBrowserTest() = default;
~ExtensionPreferenceApiUnsupportedInAshBrowserTest() override = default;
void SetUpOnMainThread() override {
ExtensionApiTest::SetUpOnMainThread();
// If the lacros service or the network settings service interface are not
// available on this version of ash-chrome, this test suite will no-op.
if (!IsServiceAvailable()) {
GTEST_SKIP() << "The Lacros service is not available.";
}
// Replace the production prefs service with a fake for testing.
mojo::Remote<crosapi::mojom::Prefs>& remote =
chromeos::LacrosService::Get()->GetRemote<crosapi::mojom::Prefs>();
remote.reset();
receiver_.Bind(remote.BindNewPipeAndPassReceiver());
}
FakePrefsAshService service_;
mojo::Receiver<crosapi::mojom::Prefs> receiver_{&service_};
};
INSTANTIATE_TEST_SUITE_P(PersistentBackground,
ExtensionPreferenceApiUnsupportedInAshBrowserTest,
::testing::Values(ContextType::kPersistentBackground));
INSTANTIATE_TEST_SUITE_P(ServiceWorker,
ExtensionPreferenceApiUnsupportedInAshBrowserTest,
::testing::Values(ContextType::kServiceWorker));
// Tests that verifies that an error message is returned when an extension is
// requesting the value of a pref that should be controlled in Ash but it's not
// supported in the Ash version due to the Ash-Lacros version skew.
IN_PROC_BROWSER_TEST_P(ExtensionPreferenceApiUnsupportedInAshBrowserTest,
UnsupportedInAsh) {
EXPECT_TRUE(RunExtensionTest("preference/unsupported_in_ash", {},
{.allow_in_incognito = false}))
<< message_;
}