// 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 "base/test/test_future.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/pref_names.h"
#include "chromeos/crosapi/mojom/prefs.mojom-shared.h"
#include "chromeos/crosapi/mojom/prefs.mojom.h"
#include "chromeos/lacros/crosapi_pref_observer.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 "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.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"
namespace {
using ContextType = extensions::ExtensionBrowserTest::ContextType;
void SetPref(crosapi::mojom::PrefPath path, base::Value value) {
base::test::TestFuture<void> future;
chromeos::LacrosService::Get()->GetRemote<crosapi::mojom::Prefs>()->SetPref(
path, std::move(value), future.GetCallback());
ASSERT_TRUE(future.Wait());
}
std::optional<base::Value> GetPref(crosapi::mojom::PrefPath path) {
base::test::TestFuture<std::optional<base::Value>> future;
chromeos::LacrosService::Get()->GetRemote<crosapi::mojom::Prefs>()->GetPref(
path, future.GetCallback());
return future.Take();
}
} // namespace
// 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::kLacrosAccessibilitySpokenFeedbackEnabled);
ASSERT_TRUE(pref);
EXPECT_TRUE(pref->IsExtensionControlled());
EXPECT_TRUE(
prefs->GetBoolean(prefs::kLacrosAccessibilitySpokenFeedbackEnabled));
const PrefService::Preference* proxy_pref =
prefs->FindPreference(proxy_config::prefs::kProxy);
ASSERT_TRUE(proxy_pref);
EXPECT_TRUE(proxy_pref->IsExtensionControlled());
EXPECT_EQ(ProxyConfigDictionary::CreateDirect(),
proxy_pref->GetValue()->GetDict());
}
void CheckPreferencesCleared() {
PrefService* prefs = profile_->GetPrefs();
const PrefService::Preference* pref =
prefs->FindPreference(prefs::kLacrosAccessibilitySpokenFeedbackEnabled);
ASSERT_TRUE(pref);
EXPECT_FALSE(pref->IsExtensionControlled());
EXPECT_FALSE(
prefs->GetBoolean(prefs::kLacrosAccessibilitySpokenFeedbackEnabled));
const PrefService::Preference* proxy_pref =
prefs->FindPreference(proxy_config::prefs::kProxy);
ASSERT_TRUE(proxy_pref);
EXPECT_FALSE(proxy_pref->IsExtensionControlled());
EXPECT_EQ(ProxyConfigDictionary::CreateSystem(),
proxy_pref->GetValue()->GetDict());
}
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 IsLacrosServiceSyncingProxyPref() {
static constexpr int kMinVersionProxyPolicy = 4;
const int version = chromeos::LacrosService::Get()
->GetInterfaceVersion<crosapi::mojom::Prefs>();
return version >= kMinVersionProxyPolicy;
}
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) {
// At start, the value in ash should not be set.
std::optional<base::Value> out_value =
GetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled);
EXPECT_FALSE(out_value.value().GetBool());
extensions::ExtensionId test_extension_id;
base::FilePath extension_path =
test_data_dir_.AppendASCII("preference/lacros");
{
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
const extensions::Extension* extension = LoadExtension(extension_path);
EXPECT_TRUE(extension) << message_;
// Save the test extension ID rather than using last_loaded_extension_id as
// toggling ChromeVox will cause a ChromeVox helper extension to be
// installed in Lacros.
test_extension_id = extension->id();
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.
out_value =
GetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled);
EXPECT_TRUE(out_value.value().GetBool());
if (IsLacrosServiceSyncingProxyPref()) {
out_value = GetPref(crosapi::mojom::PrefPath::kProxy);
EXPECT_EQ(out_value.value().GetDict(),
ProxyConfigDictionary::CreateDirect());
}
// The settings should not be reset when the extension is reloaded.
{
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
ReloadExtension(test_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_), test_extension_id);
UninstallExtension(test_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.
out_value =
GetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled);
EXPECT_FALSE(out_value.value().GetBool());
if (IsLacrosServiceSyncingProxyPref()) {
out_value = GetPref(crosapi::mojom::PrefPath::kProxy);
EXPECT_EQ(out_value.value().GetDict(),
ProxyConfigDictionary::CreateSystem());
}
}
{
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
EXPECT_TRUE(LoadExtension(extension_path));
EXPECT_TRUE(listener.WaitUntilSatisfied());
listener.Reply("");
}
CheckPreferencesCleared();
}
IN_PROC_BROWSER_TEST_P(ExtensionPreferenceApiLacrosBrowserTest,
LacrosSecondaryProfile) {
// At start, the value in ash should not be set.
std::optional<base::Value> out_value =
GetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled);
EXPECT_FALSE(out_value.value().GetBool());
// Create a secondary profile.
ProfileManager* profile_manager = g_browser_process->profile_manager();
Profile& secondary_profile = profiles::testing::CreateProfileSync(
profile_manager, profile_manager->GenerateNextProfileDirectoryPath());
ASSERT_FALSE(secondary_profile.IsMainProfile());
// Load the testing extension in secondary profile.
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener_1("ready", ReplyBehavior::kWillReply);
extensions::ChromeTestExtensionLoader loader(&secondary_profile);
base::FilePath extension_path =
test_data_dir_.AppendASCII("preference/lacros_secondary_profile_read");
scoped_refptr<const extensions::Extension> extension =
loader.LoadExtension(extension_path);
ASSERT_TRUE(extension);
EXPECT_TRUE(listener_1.WaitUntilSatisfied());
// Run the test to verify that testing extension running in secondary
// profile reads the default values of the Prefs correctly.
listener_1.Reply("run test default value");
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
// Set the pref value in ash.
SetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled,
base::Value(true));
// Verify the value is set in ash side.
out_value =
GetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled);
EXPECT_TRUE(out_value.value().GetBool());
// Reload the testing extension in the secondary profile.
ExtensionTestMessageListener listener_2("ready", ReplyBehavior::kWillReply);
extensions::TestExtensionRegistryObserver observer(
extensions::ExtensionRegistry::Get(&secondary_profile), extension->id());
extensions::ExtensionService* extension_service =
extensions::ExtensionSystem::Get(&secondary_profile)->extension_service();
extension_service->ReloadExtension(extension->id());
observer.WaitForExtensionLoaded();
EXPECT_TRUE(listener_2.WaitUntilSatisfied());
// Run the test to verify that testing extension running in secondary
// profile reads the changed value of the accessibilityFeatures correctly.
listener_2.Reply("run test changed value");
EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
// Since lacros browser tests shared the same ash instance, we need to restore
// the modified pref in ash to default before exiting the test, so that
// it won't affect other lacros browser tests.
SetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled,
base::Value(false));
out_value =
GetPref(crosapi::mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled);
EXPECT_FALSE(out_value.value().GetBool());
}
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_;
}
base::Value::Dict GetAshProxyPrefValue() {
std::optional<::base::Value> out_value =
GetPref(crosapi::mojom::PrefPath::kProxy);
return out_value.value().GetDict().Clone();
}
scoped_refptr<const extensions::Extension> InstallExtensionForProfile(
Profile* profile,
const base::FilePath& path) {
extensions::ResultCatcher catcher;
ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
scoped_refptr<const extensions::Extension> extension =
extensions::ChromeTestExtensionLoader(profile).LoadExtension(path);
EXPECT_TRUE(listener.WaitUntilSatisfied());
// Run the tests.
listener.Reply("run test");
EXPECT_TRUE(catcher.GetNextResult());
return extension;
}
void ExpectThatProxyIsControlledByExtension(Profile* profile) {
const PrefService::Preference* pref =
profile->GetPrefs()->FindPreference(proxy_config::prefs::kProxy);
EXPECT_TRUE(pref->IsExtensionControlled());
EXPECT_EQ(ProxyConfigDictionary::CreateDirect(), pref->GetValue()->GetDict());
}
void ExpectThatProxyHasDefaultValue(Profile* profile) {
const PrefService::Preference* pref =
profile->GetPrefs()->FindPreference(proxy_config::prefs::kProxy);
EXPECT_FALSE(pref->IsExtensionControlled());
EXPECT_EQ(ProxyConfigDictionary::CreateSystem(), pref->GetValue()->GetDict());
}
// Secondary profiles should apply extension set proxy at browser level, but not
// in Ash.
IN_PROC_BROWSER_TEST_P(ExtensionPreferenceApiLacrosBrowserTest,
SecondaryProfilePrefs) {
if (!IsServiceAvailable()) {
return;
}
if (!IsLacrosServiceSyncingProxyPref()) {
GTEST_SKIP() << "Skipping test because the current version of Ash does not "
"support getting the proxy preference from a Lacros "
"extension via the preferences service";
}
ProfileManager* profile_manager = g_browser_process->profile_manager();
base::FilePath path_profile =
profile_manager->GenerateNextProfileDirectoryPath();
Profile& secondary_profile =
profiles::testing::CreateProfileSync(profile_manager, path_profile);
scoped_refptr<const extensions::Extension> extension =
InstallExtensionForProfile(
&secondary_profile,
test_data_dir_.AppendASCII("preference/lacros_secondary_profile"));
// Verify that the proxy is set by the extension for the secondary profile.
ExpectThatProxyIsControlledByExtension(&secondary_profile);
// The proxy should not be set in the primary profile and Ash.
ExpectThatProxyHasDefaultValue(profile());
EXPECT_EQ(GetAshProxyPrefValue(), ProxyConfigDictionary::CreateSystem());
}
// Clearing an extension set proxy in a secondary profile should not clear the
// extension set proxy in the primary profile and Ash (if the primary profile
// has an extension which controls the proxy). The test setup:
// - Create a secondary profile;
// - Install an extension which controls the proxy pref in the primary profile;
// - Install an extension which controls the proxy pref in the secondary
// profile;
// - Verify that both profiles have extension controlled proxy prefs;
// - Uninstall the proxy controlling extension in the secondary profile;
// - Verify that the secondary profile does not have an extension set proxy;
// - Verify that the primary profile and Ash still have an extension set proxy.
// This test can be extended to other prefs for which the primary profile
// controls the value in Ash but secondary profiles only control the pref
// value at browser level.
IN_PROC_BROWSER_TEST_P(ExtensionPreferenceApiLacrosBrowserTest,
SecondaryProfilePrefsClearPref) {
if (!IsServiceAvailable()) {
return;
}
if (!IsLacrosServiceSyncingProxyPref()) {
GTEST_SKIP() << "Skipping test because the current version of Ash does not "
"support getting the proxy preference from a Lacros "
"extension via the preferences service";
}
ProfileManager* profile_manager = g_browser_process->profile_manager();
base::FilePath path_profile =
profile_manager->GenerateNextProfileDirectoryPath();
Profile& secondary_profile =
profiles::testing::CreateProfileSync(profile_manager, path_profile);
scoped_refptr<const extensions::Extension> extension_primary =
InstallExtensionForProfile(
profile(),
test_data_dir_.AppendASCII("preference/lacros_secondary_profile"));
scoped_refptr<const extensions::Extension> extension_secondary =
InstallExtensionForProfile(
&secondary_profile,
test_data_dir_.AppendASCII("preference/lacros_secondary_profile"));
ExpectThatProxyIsControlledByExtension(&secondary_profile);
ExpectThatProxyIsControlledByExtension(profile());
// Uninstall the extension in the secondary profile and test that Ash is still
// returning the pref set by the extension running in the Lacros primary
// profile.
{
extensions::TestExtensionRegistryObserver observer(
extensions::ExtensionRegistry::Get(&secondary_profile),
extension_secondary->id());
auto* service_ = extensions::ExtensionSystem::Get(&secondary_profile)
->extension_service();
service_->UninstallExtension(extension_secondary->id(),
extensions::UNINSTALL_REASON_FOR_TESTING,
NULL);
observer.WaitForExtensionUninstalled();
}
ExpectThatProxyHasDefaultValue(&secondary_profile);
ExpectThatProxyIsControlledByExtension(profile());
EXPECT_EQ(GetAshProxyPrefValue(), ProxyConfigDictionary::CreateDirect());
// Uninstall the extension in the primary profile.
{
extensions::TestExtensionRegistryObserver observer(
extensions::ExtensionRegistry::Get(profile()), extension_primary->id());
auto* service_ =
extensions::ExtensionSystem::Get(profile())->extension_service();
service_->UninstallExtension(extension_primary->id(),
extensions::UNINSTALL_REASON_FOR_TESTING,
NULL);
observer.WaitForExtensionUninstalled();
}
EXPECT_EQ(GetAshProxyPrefValue(), ProxyConfigDictionary::CreateSystem());
}
// 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(std::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(std::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_;
}