chromium/chrome/browser/ui/webui/ash/settings/pages/privacy/privacy_hub_handler_unittest.cc

// 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/webui/ash/settings/pages/privacy/privacy_hub_handler.h"

#include "ash/constants/ash_features.h"
#include "base/containers/adapters.h"
#include "base/ranges/algorithm.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "chrome/browser/ash/privacy_hub/privacy_hub_hats_trigger.h"
#include "chrome/browser/ash/privacy_hub/privacy_hub_util.h"
#include "chrome/common/chrome_features.h"
#include "chromeos/ash/components/audio/cras_audio_handler.h"
#include "content/public/test/test_web_ui.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::settings {

namespace {
class TestPrivacyHubHandler : public PrivacyHubHandler {
 public:
  using content::WebUIMessageHandler::set_web_ui;

  using PrivacyHubHandler::HandleInitialCameraLedFallbackState;
  using PrivacyHubHandler::HandleInitialMicrophoneSwitchState;
  using PrivacyHubHandler::HandlePrivacyPageClosed;
  using PrivacyHubHandler::HandlePrivacyPageOpened;
};

using cps = cros::mojom::CameraPrivacySwitchState;
}  // namespace

class PrivacyHubHandlerTest : public testing::Test {
  // This has to go before privacy_hub_handler_ because in the
  // CameraHalDispatcherImpl constructor a call to
  // base::SequencedTaskRunner::GetCurrentDefault() is made which requires a
  // task_environment. Initialization order of the members takes care
  // of providing this before privacy_hub_handler_ is constructed.
  base::test::SingleThreadTaskEnvironment task_environment_;

  // This has to go before privacy_hub_handler_ because PrivacyHubHandler
  // constructor  requires CrasAudioHandler to be initialized.
  // ScopedCrasAudioHandlerForTesting is a helper class that initializes
  // CrasAudioHandler in it's constructor.
  ScopedCrasAudioHandlerForTesting cras_audio_handler_;

  // Has to go before privacy_hub_handler_ as it references its
  // address and destruction order guarantees no invalid pointers.
  content::TestWebUI web_ui_;

 public:
  PrivacyHubHandlerTest()
      : this_test_name_(
            testing::UnitTest::GetInstance()->current_test_info()->name()) {
    privacy_hub_handler_.set_web_ui(&web_ui_);
    privacy_hub_handler_.AllowJavascriptForTesting();

    feature_list_.InitWithFeatures({features::kCrosPrivacyHub}, {});
  }

  [[nodiscard]] base::Value GetLastWebUIListenerData(
      const std::string& callback) const {
    return GetLastWebUIData("cr.webUIListenerCallback", callback);
  }

  [[nodiscard]] base::Value GetLastWebUIResponse(
      const std::string& callback) const {
    return GetLastWebUIData("cr.webUIResponse", callback);
  }

  TestPrivacyHubHandler privacy_hub_handler_;
  const std::string this_test_name_;

 protected:
  void ExpectValueMatchesBoolParam(bool param, const base::Value& value) const {
    ASSERT_TRUE(value.is_bool());
    EXPECT_EQ(param, value.GetBool());
  }

 private:
  [[nodiscard]] base::Value GetLastWebUIData(
      const std::string& function_name,
      const std::string& callback_name) const {
    for (const auto& data : base::Reversed(web_ui_.call_data())) {
      const std::string* name = data->arg1()->GetIfString();

      if (data->function_name() != function_name || !name ||
          *name != callback_name) {
        continue;
      }

      // Assume that the data is stored in the last valid arg.
      for (const auto& arg : base::Reversed(data->args())) {
        if (&arg != data->arg1()) {
          return arg.Clone();
        }
      }
    }

    ADD_FAILURE() << "None of the " << web_ui_.call_data().size()
                  << " CallData objects matched for function '" << function_name
                  << "' and callback '" << callback_name
                  << "' with a valid arg";
    return base::Value();
  }

  base::test::ScopedFeatureList feature_list_;
};

class PrivacyHubHandlerMicrophoneTest
    : public PrivacyHubHandlerTest,
      public testing::WithParamInterface<bool> {
 public:
  void SetParamValueMicrophoneMute() const {
    // Have to set it to not-param once before to ensure the observers are
    // triggered from MicrophoneMuteSwitchMonitor.
    ui::MicrophoneMuteSwitchMonitor::Get()->SetMicrophoneMuteSwitchValue(
        !GetParam());
    ui::MicrophoneMuteSwitchMonitor::Get()->SetMicrophoneMuteSwitchValue(
        GetParam());
  }

  void ExpectValueMatchesBoolParam(const base::Value& value) const {
    PrivacyHubHandlerTest::ExpectValueMatchesBoolParam(GetParam(), value);
  }
};

class PrivacyHubHandlerCameraLedFallbackTest
    : public PrivacyHubHandlerTest,
      public testing::WithParamInterface<bool> {
 public:
  PrivacyHubHandlerCameraLedFallbackTest()
      : scoped_camera_led_fallback_(GetParam()) {}

  void ExpectValueMatchesBoolParam(const base::Value& value) const {
    PrivacyHubHandlerTest::ExpectValueMatchesBoolParam(GetParam(), value);
  }

 private:
  privacy_hub_util::ScopedCameraLedFallbackForTesting
      scoped_camera_led_fallback_;
};

class PrivacyHubHandlerHatsTest : public PrivacyHubHandlerTest {
 public:
  PrivacyHubHandlerHatsTest() {
    feature_list_.InitAndEnableFeature(
        ::features::kHappinessTrackingPrivacyHubPostLaunch);
  }

  bool IsTimerStarted() {
    return PrivacyHubHatsTrigger::Get().GetTimerForTesting().IsRunning();
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

class PrivacyHubHandlerCameraSwitchTest
    : public PrivacyHubHandlerTest,
      public testing::WithParamInterface<bool> {
 public:
  void ExpectValueMatchesBoolParam(const base::Value& value) const {
    PrivacyHubHandlerTest::ExpectValueMatchesBoolParam(GetParam(), value);
  }
};

TEST_P(PrivacyHubHandlerMicrophoneTest,
       MicrophoneHardwarePrivacySwitchChanged) {
  privacy_hub_handler_.MicrophoneHardwareToggleChanged(GetParam());

  const base::Value data =
      GetLastWebUIListenerData("microphone-hardware-toggle-changed");

  ExpectValueMatchesBoolParam(data);
}

TEST_P(PrivacyHubHandlerMicrophoneTest, HandleInitialMicrophoneSwitchState) {
  SetParamValueMicrophoneMute();

  privacy_hub_handler_.HandleInitialMicrophoneSwitchState(
      base::Value::List().Append(this_test_name_));

  const base::Value data = GetLastWebUIResponse(this_test_name_);

  ExpectValueMatchesBoolParam(data);
}

TEST_P(PrivacyHubHandlerCameraSwitchTest,
       ForceDisableCameraSwitchSwitchChanged) {
  privacy_hub_handler_.SetForceDisableCameraSwitch(GetParam());

  const base::Value data =
      GetLastWebUIListenerData("force-disable-camera-switch");

  ExpectValueMatchesBoolParam(data);
}

INSTANTIATE_TEST_SUITE_P(HardwareSwitchStates,
                         PrivacyHubHandlerMicrophoneTest,
                         testing::Values(true, false),
                         testing::PrintToStringParamName());

INSTANTIATE_TEST_SUITE_P(HardwareSwitchStates,
                         PrivacyHubHandlerCameraSwitchTest,
                         testing::Values(true, false),
                         testing::PrintToStringParamName());

TEST_P(PrivacyHubHandlerCameraLedFallbackTest,
       HandleInitialCameraLedCallbackState) {
  base::Value::List args;
  args.Append(this_test_name_);

  privacy_hub_handler_.HandleInitialCameraLedFallbackState(args);

  const base::Value data = GetLastWebUIResponse(this_test_name_);

  ExpectValueMatchesBoolParam(data);
}

INSTANTIATE_TEST_SUITE_P(CameraLedFallback,
                         PrivacyHubHandlerCameraLedFallbackTest,
                         testing::Values(true, false),
                         testing::PrintToStringParamName());

TEST_F(PrivacyHubHandlerHatsTest, OnlyTriggerHatsIfPageWasVisitedLongEnough) {
  const base::Value::List args;

  EXPECT_FALSE(IsTimerStarted());

  // We trigger the HaTS survey on the leave event but the user hasn't visited
  // the page yet.
  privacy_hub_handler_.HandlePrivacyPageClosed(args);
  EXPECT_FALSE(IsTimerStarted());

  // User goes to the page.
  privacy_hub_handler_.HandlePrivacyPageOpened(args);
  EXPECT_FALSE(IsTimerStarted());

  // Simulate the user stays on the page for 5 seconds.
  privacy_hub_handler_.SetPrivacyPageOpenedTimeStampForTesting(
      base::TimeTicks::Now() - base::Seconds(5));
  EXPECT_FALSE(IsTimerStarted());

  // And leaves it again, now the survey should be triggered.
  privacy_hub_handler_.HandlePrivacyPageClosed(args);
  EXPECT_TRUE(IsTimerStarted());
}

TEST_F(PrivacyHubHandlerHatsTest, DontTriggerHatsIfUserLeftEarly) {
  const base::Value::List args;

  EXPECT_FALSE(IsTimerStarted());

  // We trigger the HaTS survey on the leave event but the user hasn't visited
  // the page yet.
  privacy_hub_handler_.HandlePrivacyPageClosed(args);
  EXPECT_FALSE(IsTimerStarted());

  // User goes to the page.
  privacy_hub_handler_.HandlePrivacyPageOpened(args);
  EXPECT_FALSE(IsTimerStarted());

  // And leaves it again immediately, now the survey shouldn't be triggered.
  privacy_hub_handler_.HandlePrivacyPageClosed(args);
  EXPECT_FALSE(IsTimerStarted());
}

TEST_F(PrivacyHubHandlerTest, MicrophoneMutedBySecurityCurtainChanged) {
  privacy_hub_handler_.OnInputMutedBySecurityCurtainChanged(true);

  ExpectValueMatchesBoolParam(
      true,
      GetLastWebUIListenerData("microphone-muted-by-security-curtain-changed"));

  privacy_hub_handler_.OnInputMutedBySecurityCurtainChanged(false);

  ExpectValueMatchesBoolParam(
      false,
      GetLastWebUIListenerData("microphone-muted-by-security-curtain-changed"));
}

#if DCHECK_IS_ON()
using PrivacyHubHandlerDeathTest = PrivacyHubHandlerTest;

TEST_F(PrivacyHubHandlerDeathTest,
       HandleInitialMicrophoneSwitchStateNoCallbackId) {
  base::Value::List args;

  EXPECT_DEATH(privacy_hub_handler_.HandleInitialMicrophoneSwitchState(args),
               ".*Callback ID is required.*");
}

TEST_F(PrivacyHubHandlerDeathTest, HandleInitialMicrophoneSwitchStateWithArgs) {
  base::Value::List args;
  args.Append(this_test_name_);
  args.Append(base::Value());

  EXPECT_DEATH(privacy_hub_handler_.HandleInitialMicrophoneSwitchState(args),
               ".*Did not expect arguments.*");
}

TEST_F(PrivacyHubHandlerDeathTest, HandlePrivacyPageOpened) {
  base::Value::List args;
  args.Append(this_test_name_);

  EXPECT_DEATH(privacy_hub_handler_.HandlePrivacyPageOpened(args), ".*empty.*");
}

TEST_F(PrivacyHubHandlerDeathTest, HandlePrivacyPageClosed) {
  base::Value::List args;
  args.Append(this_test_name_);

  EXPECT_DEATH(privacy_hub_handler_.HandlePrivacyPageClosed(args), ".*empty.*");
}

TEST_F(PrivacyHubHandlerDeathTest, OnlyTriggerHatsIfFeatureIsEnabled) {
  const base::Value::List args;
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(
      ::features::kHappinessTrackingPrivacyHubPostLaunch);

  // User goes to the page.
  EXPECT_DEATH(privacy_hub_handler_.HandlePrivacyPageOpened(args),
               "base::FeatureList::IsEnabled");
  EXPECT_DEATH(privacy_hub_handler_.HandlePrivacyPageClosed(args),
               "base::FeatureList::IsEnabled");
}

#endif

}  // namespace ash::settings