chromium/chrome/browser/ash/crosapi/prefs_ash_unittest.cc

// Copyright 2021 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/ash/crosapi/prefs_ash.h"

#include <memory>
#include <optional>

#include "ash/constants/ash_pref_names.h"
#include "base/functional/callback_helpers.h"
#include "base/test/bind.h"
#include "chrome/browser/ash/settings/scoped_testing_cros_settings.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "components/metrics/metrics_pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace crosapi {
namespace {

class TestObserver : public mojom::PrefObserver {
 public:
  TestObserver() = default;
  TestObserver(const TestObserver&) = delete;
  TestObserver& operator=(const TestObserver&) = delete;
  ~TestObserver() override = default;

  // crosapi::mojom::PrefObserver:
  void OnPrefChanged(base::Value value) override { value_ = std::move(value); }

  std::optional<base::Value> value_;
  mojo::Receiver<mojom::PrefObserver> receiver_{this};
};

}  // namespace

class PrefsAshTest : public testing::Test {
 public:
  PrefsAshTest() = default;
  PrefsAshTest(const PrefsAshTest&) = delete;
  PrefsAshTest& operator=(const PrefsAshTest&) = delete;
  ~PrefsAshTest() override = default;

  void SetUp() override { ASSERT_TRUE(testing_profile_manager_.SetUp()); }

  TestingPrefServiceSimple* local_state() {
    return testing_profile_manager_.local_state()->Get();
  }
  ProfileManager* profile_manager() {
    return testing_profile_manager_.profile_manager();
  }

  Profile* CreateProfile() {
    return testing_profile_manager_.CreateTestingProfile(std::string());
  }

  content::BrowserTaskEnvironment task_environment_;

 private:
  TestingProfileManager testing_profile_manager_{
      TestingBrowserProcess::GetGlobal()};
};

void GetExtensionPrefWithControl(mojo::Remote<mojom::Prefs>& prefs_remote,
                                 mojom::PrefPath path,
                                 base::Value* get_value,
                                 mojom::PrefControlState* get_control) {
  prefs_remote->GetExtensionPrefWithControl(
      path, base::BindLambdaForTesting([&](std::optional<base::Value> value,
                                           mojom::PrefControlState control) {
        *get_value = std::move(*value);
        *get_control = control;
      }));
  prefs_remote.FlushForTesting();
}

void GetPref(mojo::Remote<mojom::Prefs>& prefs_remote,
             mojom::PrefPath path,
             base::Value* get_value) {
  prefs_remote->GetPref(
      path, base::BindLambdaForTesting([&](std::optional<base::Value> value) {
        *get_value = std::move(*value);
      }));
  prefs_remote.FlushForTesting();
}

TEST_F(PrefsAshTest, LocalStatePrefs) {
  local_state()->SetBoolean(metrics::prefs::kMetricsReportingEnabled, false);
  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(CreateProfile());
  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path = mojom::PrefPath::kMetricsReportingEnabled;

  // Get returns value.
  base::Value get_value;
  GetPref(prefs_remote, path, &get_value);
  prefs_remote.FlushForTesting();
  EXPECT_FALSE(get_value.GetBool());

  // Set updates value.
  prefs_remote->SetPref(path, base::Value(true), base::DoNothing());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(
      local_state()->GetBoolean(metrics::prefs::kMetricsReportingEnabled));

  // Adding an observer results in it being fired with the current state.
  EXPECT_FALSE(prefs_ash.local_state_registrar_.IsObserved(
      metrics::prefs::kMetricsReportingEnabled));
  auto observer1 = std::make_unique<TestObserver>();
  prefs_remote->AddObserver(path,
                            observer1->receiver_.BindNewPipeAndPassRemote());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(observer1->value_->GetBool());
  EXPECT_TRUE(prefs_ash.local_state_registrar_.IsObserved(
      metrics::prefs::kMetricsReportingEnabled));
  EXPECT_EQ(1u, prefs_ash.observers_[path].size());

  // Multiple observers is ok.
  auto observer2 = std::make_unique<TestObserver>();
  prefs_remote->AddObserver(path,
                            observer2->receiver_.BindNewPipeAndPassRemote());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(observer2->value_->GetBool());
  EXPECT_EQ(2u, prefs_ash.observers_[path].size());

  // Observer should be notified when value changes.
  local_state()->SetBoolean(metrics::prefs::kMetricsReportingEnabled, false);
  task_environment_.RunUntilIdle();
  EXPECT_FALSE(observer1->value_->GetBool());
  EXPECT_FALSE(observer2->value_->GetBool());

  // Disconnect should remove PrefChangeRegistrar.
  observer1.reset();
  prefs_remote.FlushForTesting();
  EXPECT_EQ(1u, prefs_ash.observers_[path].size());
  observer2.reset();
  prefs_remote.FlushForTesting();
  EXPECT_EQ(0u, prefs_ash.observers_[path].size());
  EXPECT_FALSE(prefs_ash.local_state_registrar_.IsObserved(
      metrics::prefs::kMetricsReportingEnabled));
}

TEST_F(PrefsAshTest, LocalStatePref_SystemTracing) {
  const char* pref_name = ash::prefs::kDeviceSystemWideTracingEnabled;

  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(CreateProfile());
  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path = mojom::PrefPath::kDeviceSystemWideTracingEnabled;

  // Get returns value.
  base::Value get_value;
  // Tests the default pref value.
  GetPref(prefs_remote, path, &get_value);
  EXPECT_TRUE(get_value.GetBool());

  // Tests the GetPref() method.
  local_state()->SetBoolean(pref_name, false);
  GetPref(prefs_remote, path, &get_value);
  prefs_remote.FlushForTesting();
  EXPECT_FALSE(get_value.GetBool());

  local_state()->SetBoolean(pref_name, true);
  GetPref(prefs_remote, path, &get_value);
  EXPECT_TRUE(get_value.GetBool());

  // Tests observing pref changes.
  auto observer = std::make_unique<TestObserver>();
  prefs_remote->AddObserver(path,
                            observer->receiver_.BindNewPipeAndPassRemote());
  task_environment_.RunUntilIdle();
  EXPECT_TRUE(observer->value_->GetBool());

  local_state()->SetBoolean(pref_name, false);
  task_environment_.RunUntilIdle();
  EXPECT_FALSE(observer->value_->GetBool());
}

TEST_F(PrefsAshTest, ProfilePrefs) {
  Profile* const profile = CreateProfile();
  PrefService* const profile_prefs = profile->GetPrefs();
  profile_prefs->SetBoolean(ash::prefs::kAccessibilitySpokenFeedbackEnabled,
                            false);
  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(profile);

  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path = mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled;

  // Get returns value.
  base::Value get_value;
  GetPref(prefs_remote, path, &get_value);
  EXPECT_FALSE(get_value.GetBool());

  // Set updates value.
  prefs_remote->SetPref(path, base::Value(true), base::DoNothing());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(profile_prefs->GetBoolean(
      ash::prefs::kAccessibilitySpokenFeedbackEnabled));

  // Adding an observer results in it being fired with the current state.
  TestObserver observer;
  prefs_remote->AddObserver(path,
                            observer.receiver_.BindNewPipeAndPassRemote());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(observer.value_->GetBool());
}

TEST_F(PrefsAshTest, GetUnknown) {
  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(CreateProfile());
  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path = mojom::PrefPath::kUnknown;

  // Get for an unknown value returns std::nullopt.
  bool has_value = true;
  prefs_remote->GetPref(
      path, base::BindLambdaForTesting([&](std::optional<base::Value> value) {
        has_value = value.has_value();
      }));
  prefs_remote.FlushForTesting();
  EXPECT_FALSE(has_value);

  // Set or AddObserver for an unknown value does nothing.
  prefs_remote->SetPref(path, base::Value(false), base::DoNothing());
  TestObserver observer;
  prefs_remote->AddObserver(path,
                            observer.receiver_.BindNewPipeAndPassRemote());
  prefs_remote.FlushForTesting();
  EXPECT_FALSE(observer.value_.has_value());
}

TEST_F(PrefsAshTest, GetWithControlUnknown) {
  Profile* const profile = CreateProfile();
  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(profile);
  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path = mojom::PrefPath::kUnknown;

  // Get for an unknown value returns std::nullopt.
  bool has_value = true;
  mojom::PrefControlState get_control;
  prefs_remote->GetExtensionPrefWithControl(
      path, base::BindLambdaForTesting([&](std::optional<base::Value> value,
                                           mojom::PrefControlState control) {
        has_value = value.has_value();
        get_control = control;
      }));
  prefs_remote.FlushForTesting();
  EXPECT_FALSE(has_value);
  EXPECT_EQ(get_control, mojom::PrefControlState::kDefaultUnknown);
}

TEST_F(PrefsAshTest, ExtensionPrefsControllable) {
  local_state()->registry()->RegisterBooleanPref(
      ash::prefs::kDockedMagnifierEnabled, false);

  Profile* const profile = CreateProfile();
  PrefService* const profile_prefs = profile->GetPrefs();
  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(profile);

  profile_prefs->SetBoolean(ash::prefs::kDockedMagnifierEnabled, true);

  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path = mojom::PrefPath::kDockedMagnifierEnabled;

  // Get returns value.
  base::Value get_value;
  GetPref(prefs_remote, path, &get_value);
  EXPECT_TRUE(get_value.GetBool());

  // GetWithControl shows this can be controlled by lacros because extensions
  // have higher precedence than profile prefs.
  mojom::PrefControlState get_control;
  GetExtensionPrefWithControl(prefs_remote, path, &get_value, &get_control);

  EXPECT_EQ(get_control, mojom::PrefControlState::kLacrosExtensionControllable);
  EXPECT_TRUE(get_value.GetBool());
}

TEST_F(PrefsAshTest, ExtensionPrefsGetSetClear) {
  local_state()->registry()->RegisterBooleanPref(
      ash::prefs::kDockedMagnifierEnabled, false);

  Profile* const profile = CreateProfile();
  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(profile);

  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path = mojom::PrefPath::kDockedMagnifierEnabled;

  prefs_remote->SetPref(path, base::Value(true), base::DoNothing());
  prefs_remote.FlushForTesting();

  base::Value get_value;
  mojom::PrefControlState get_control;

  GetExtensionPrefWithControl(prefs_remote, path, &get_value, &get_control);

  // Controlled by lacros as it was set above.
  EXPECT_EQ(get_control, mojom::PrefControlState::kLacrosExtensionControlled);
  EXPECT_TRUE(get_value.GetBool());

  // Clear Extension controlled pref.
  prefs_remote->ClearExtensionControlledPref(path, base::DoNothing());
  prefs_remote.FlushForTesting();

  GetExtensionPrefWithControl(prefs_remote, path, &get_value, &get_control);

  // Controllable by lacros, as it was unset above. No longer enabled as it
  // was cleared.
  EXPECT_EQ(get_control, mojom::PrefControlState::kLacrosExtensionControllable);
  EXPECT_FALSE(get_value.GetBool());
}

TEST_F(PrefsAshTest, ExtensionPrefsClearNonExtensionPref) {
  local_state()->registry()->RegisterBooleanPref(
      ash::prefs::kAccessibilitySpokenFeedbackEnabled, false);

  Profile* const profile = CreateProfile();
  PrefService* const profile_prefs = profile->GetPrefs();
  PrefsAsh prefs_ash(profile_manager(), local_state());
  prefs_ash.OnPrimaryProfileReadyForTesting(profile);

  profile_prefs->SetBoolean(ash::prefs::kAccessibilitySpokenFeedbackEnabled,
                            true);

  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  // Note this is the non-extension PrefPath.
  mojom::PrefPath path = mojom::PrefPath::kAccessibilitySpokenFeedbackEnabled;

  // Does nothing since this is not an extension controlled pref.
  prefs_remote->ClearExtensionControlledPref(path, base::DoNothing());
  prefs_remote.FlushForTesting();

  // Get returns value.
  base::Value get_value;
  GetPref(prefs_remote, path, &get_value);
  EXPECT_TRUE(get_value.GetBool());
}

TEST_F(PrefsAshTest, CrosSettingsPrefs) {
  ash::ScopedTestingCrosSettings scoped_testing_cros_settings;

  PrefsAsh prefs_ash(profile_manager(), local_state());
  mojo::Remote<mojom::Prefs> prefs_remote;
  prefs_ash.BindReceiver(prefs_remote.BindNewPipeAndPassReceiver());
  mojom::PrefPath path =
      mojom::PrefPath::kAttestationForContentProtectionEnabled;

  // Get returns value, which defaults to true.
  base::Value get_value;
  GetPref(prefs_remote, path, &get_value);
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(get_value.GetBool());

  // Set does not update values for CrosSettings.
  prefs_remote->SetPref(path, base::Value(false), base::DoNothing());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(scoped_testing_cros_settings.device_settings()
                  ->Get(ash::kAttestationForContentProtectionEnabled)
                  ->GetBool());

  // Adding an observer results in it being fired with the current state.
  auto observer1 = std::make_unique<TestObserver>();
  prefs_remote->AddObserver(path,
                            observer1->receiver_.BindNewPipeAndPassRemote());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(observer1->value_->GetBool());
  EXPECT_EQ(1u, prefs_ash.cros_settings_subs_.count(path));
  EXPECT_EQ(1u, prefs_ash.observers_[path].size());

  // Multiple observers is ok.
  auto observer2 = std::make_unique<TestObserver>();
  prefs_remote->AddObserver(path,
                            observer2->receiver_.BindNewPipeAndPassRemote());
  prefs_remote.FlushForTesting();
  EXPECT_TRUE(observer2->value_->GetBool());
  EXPECT_EQ(2u, prefs_ash.observers_[path].size());

  // Observer should be notified when value changes.
  scoped_testing_cros_settings.device_settings()->SetBoolean(
      ash::kAttestationForContentProtectionEnabled, false);
  task_environment_.RunUntilIdle();
  prefs_remote.FlushForTesting();
  EXPECT_FALSE(observer1->value_->GetBool());
  EXPECT_FALSE(observer2->value_->GetBool());

  // Disconnect should remove CallbackListSubscription.
  observer1.reset();
  prefs_remote.FlushForTesting();
  EXPECT_EQ(1u, prefs_ash.observers_[path].size());
  observer2.reset();
  prefs_remote.FlushForTesting();
  EXPECT_EQ(0u, prefs_ash.observers_[path].size());
  EXPECT_EQ(0u, prefs_ash.cros_settings_subs_.count(path));
}

}  // namespace crosapi