chromium/ash/system/privacy_hub/privacy_hub_geolocation_activity_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/privacy_hub/geolocation_privacy_switch_controller.h"

#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/ash_switches.h"
#include "ash/constants/geolocation_access_level.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/privacy_hub/privacy_hub_controller.h"
#include "ash/system/privacy_hub/privacy_hub_metrics.h"
#include "ash/system/privacy_hub/privacy_hub_notification.h"
#include "ash/system/privacy_hub/privacy_hub_notification_controller.h"
#include "ash/system/privacy_hub/sensor_disabled_notification_delegate.h"
#include "ash/test/ash_test_base.h"
#include "base/command_line.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/public/cpp/notification.h"

using testing::_;

namespace ash {

namespace {

message_center::Notification* FindNotification() {
  return message_center::MessageCenter::Get()->FindNotificationById(
      PrivacyHubNotificationController::kGeolocationSwitchNotificationId);
}

}  // namespace

class PrivacyHubGeolocationTestBase : public AshTestBase {
public:
  PrivacyHubGeolocationTestBase()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
    scoped_feature_list_.InitWithFeatures(
        { features::kCrosPrivacyHub}, {});
  }

  ~PrivacyHubGeolocationTestBase() override = default;

  // AshTest:
  void SetUp() override {
    AshTestBase::SetUp();
    controller_ = GeolocationPrivacySwitchController::Get();
  }

  void SetAccessLevel(GeolocationAccessLevel access_level) {
    Shell::Get()->session_controller()->GetActivePrefService()->SetInteger(
        prefs::kUserGeolocationAccessLevel, static_cast<int>(access_level));
  }

  GeolocationAccessLevel GetAccessLevel() {
    return static_cast<GeolocationAccessLevel>(
        Shell::Get()->session_controller()->GetActivePrefService()->GetInteger(
            prefs::kUserGeolocationAccessLevel));
  }

 protected:
  raw_ptr<GeolocationPrivacySwitchController, DanglingUntriaged> controller_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

class PrivacyHubGeolocationActivityTest : public PrivacyHubGeolocationTestBase,
      public testing::WithParamInterface<GeolocationAccessLevel> {
 public:
  // TODO(b/323169598): Review all uses of this function and rewrite the tests
  // to check all access levels using GetAccessLevel().
  void SetUserPref(bool allowed) {
    GeolocationAccessLevel access_level;
    if (allowed) {
      access_level = GeolocationAccessLevel::kAllowed;
    } else {
      access_level = static_cast<GeolocationAccessLevel>(GetParam());
      ASSERT_NE(access_level, GeolocationAccessLevel::kAllowed);
    }
    SetAccessLevel(access_level);
  }

  const base::HistogramTester histogram_tester_;
};

TEST_P(PrivacyHubGeolocationActivityTest, GetActiveAppsTest) {
  EXPECT_TRUE(features::IsCrosPrivacyHubLocationEnabled());
  const std::vector<std::string> app_names{"App1", "App2", "App3"};
  const std::vector<std::u16string> app_names_u16{u"App1", u"App2", u"App3"};
  EXPECT_EQ(controller_->GetActiveApps(3), (std::vector<std::u16string>{}));
  controller_->TrackGeolocationAttempted(app_names[0]);
  EXPECT_EQ(controller_->GetActiveApps(3),
            (std::vector<std::u16string>{app_names_u16[0]}));
  controller_->TrackGeolocationAttempted(app_names[1]);
  EXPECT_EQ(controller_->GetActiveApps(3),
            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
  controller_->TrackGeolocationAttempted(app_names[1]);
  EXPECT_EQ(controller_->GetActiveApps(3),
            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
  controller_->TrackGeolocationAttempted(app_names[2]);
  EXPECT_EQ(controller_->GetActiveApps(3), app_names_u16);
  controller_->TrackGeolocationRelinquished(app_names[2]);
  EXPECT_EQ(controller_->GetActiveApps(3),
            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
  controller_->TrackGeolocationRelinquished(app_names[1]);
  EXPECT_EQ(controller_->GetActiveApps(3),
            (std::vector<std::u16string>{app_names_u16[0], app_names_u16[1]}));
  controller_->TrackGeolocationRelinquished(app_names[1]);
  EXPECT_EQ(controller_->GetActiveApps(3),
            (std::vector<std::u16string>{app_names_u16[0]}));
  controller_->TrackGeolocationRelinquished(app_names[0]);
  EXPECT_EQ(controller_->GetActiveApps(3), (std::vector<std::u16string>{}));
}

TEST_P(PrivacyHubGeolocationActivityTest, NotificationOnActivityChangeTest) {
  const std::string app_name = "app";
  SetUserPref(false);
  EXPECT_FALSE(FindNotification());
  controller_->TrackGeolocationAttempted(app_name);
  EXPECT_TRUE(FindNotification());
  controller_->TrackGeolocationRelinquished(app_name);
  EXPECT_FALSE(FindNotification());
}

TEST_P(PrivacyHubGeolocationActivityTest,
       NotificationOnPreferenceChangeTest) {
  const std::string app_name = "app";
  SetUserPref(true);
  controller_->TrackGeolocationAttempted(app_name);
  EXPECT_FALSE(FindNotification());
  SetUserPref(false);
  EXPECT_TRUE(FindNotification());
  SetUserPref(true);
  EXPECT_FALSE(FindNotification());
}

TEST_P(PrivacyHubGeolocationActivityTest, ClickOnNotificationTest) {
  const std::string app_name = "app";
  SetUserPref(false);
  EXPECT_TRUE(features::IsCrosPrivacyHubLocationEnabled());
  EXPECT_TRUE(controller_);
  controller_->TrackGeolocationAttempted(app_name);

  const auto kGeolocationAccessLevels = {
      GeolocationAccessLevel::kDisallowed, GeolocationAccessLevel::kAllowed,
      GeolocationAccessLevel::kOnlyAllowedForSystem};
  ASSERT_EQ(static_cast<unsigned long>(GeolocationAccessLevel::kMaxValue) + 1,
            kGeolocationAccessLevels.size());

  // We didn't log any notification clicks so far.
  for (auto access_level : kGeolocationAccessLevels) {
    EXPECT_EQ(0,
              histogram_tester_.GetBucketCount(
                  privacy_hub_metrics::
                      kPrivacyHubGeolocationAccessLevelChangedFromNotification,
                  access_level));
  }
  EXPECT_TRUE(FindNotification());
  EXPECT_NE(GetAccessLevel(), GeolocationAccessLevel::kAllowed);

  // Click on the notification button.
  message_center::MessageCenter::Get()->ClickOnNotificationButton(
      PrivacyHubNotificationController::kGeolocationSwitchNotificationId, 0);
  // This must change the user pref.
  EXPECT_EQ(GetAccessLevel(), GeolocationAccessLevel::kAllowed);
  // The notification should be cleared after it has been clicked on.
  EXPECT_FALSE(FindNotification());

  // The histograms were updated.
  EXPECT_EQ(1, histogram_tester_.GetBucketCount(
                   privacy_hub_metrics::
                       kPrivacyHubGeolocationAccessLevelChangedFromNotification,
                   GeolocationAccessLevel::kAllowed));
  EXPECT_EQ(0, histogram_tester_.GetBucketCount(
                   privacy_hub_metrics::
                       kPrivacyHubGeolocationAccessLevelChangedFromNotification,
                   GeolocationAccessLevel::kDisallowed));
  EXPECT_EQ(0, histogram_tester_.GetBucketCount(
                   privacy_hub_metrics::
                       kPrivacyHubGeolocationAccessLevelChangedFromNotification,
                   GeolocationAccessLevel::kOnlyAllowedForSystem));
}

INSTANTIATE_TEST_SUITE_P(All,
                         PrivacyHubGeolocationActivityTest,
                         testing::Values( GeolocationAccessLevel::kDisallowed, GeolocationAccessLevel::kOnlyAllowedForSystem));

using BooleanSyncTransitionTableRow = std::tuple<GeolocationAccessLevel,
                                                 GeolocationAccessLevel,
                                                 bool,
                                                 GeolocationAccessLevel,
                                                 GeolocationAccessLevel>;

class PrivacyHubGeolocationApplyArcLocationUpdatesTest
    : public PrivacyHubGeolocationTestBase,
      public testing::WithParamInterface<BooleanSyncTransitionTableRow> {
 public:
  GeolocationAccessLevel PreviousAccessLevel() const {
    return std::get<0>(GetParam());
  }
  GeolocationAccessLevel CurrentAccessLevel() const {
    return std::get<1>(GetParam());
  }
  bool IncomingValueToBeSynced() const { return std::get<2>(GetParam()); }
  GeolocationAccessLevel ExpectedNewPreviousAccessLevel() const {
    return std::get<3>(GetParam());
  }
  GeolocationAccessLevel ExpectedNewAccessLevel() const {
    return std::get<4>(GetParam());
  }
};

TEST_P(PrivacyHubGeolocationApplyArcLocationUpdatesTest, UpdateTest) {
  ASSERT_NE(CurrentAccessLevel(), PreviousAccessLevel());
  // Test initial values
  EXPECT_EQ(GeolocationAccessLevel::kAllowed, controller_->AccessLevel());
  EXPECT_EQ(GeolocationAccessLevel::kDisallowed,
            controller_->PreviousAccessLevel());

  SetAccessLevel(PreviousAccessLevel());
  SetAccessLevel(CurrentAccessLevel());
  EXPECT_EQ(CurrentAccessLevel(), controller_->AccessLevel());
  EXPECT_EQ(PreviousAccessLevel(), controller_->PreviousAccessLevel());

  controller_->ApplyArcLocationUpdate(IncomingValueToBeSynced());
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(ExpectedNewAccessLevel(), controller_->AccessLevel());
  EXPECT_EQ(ExpectedNewPreviousAccessLevel(),
            controller_->PreviousAccessLevel());
}

using BooleanSyncTransitionTable = std::vector<BooleanSyncTransitionTableRow>;
// Utility function to generate all possible perameter combinations for the
// PrivacyHubGeolocationApplyArcLocationUpdatesTest test suite.
BooleanSyncTransitionTable GenerateTransitionTable() {
  BooleanSyncTransitionTable table;
  const std::array<GeolocationAccessLevel, 3> all_levels{
      GeolocationAccessLevel::kAllowed, GeolocationAccessLevel::kDisallowed,
      GeolocationAccessLevel::kOnlyAllowedForSystem};
  const auto is_blocking = [](GeolocationAccessLevel level) {
    return (level != GeolocationAccessLevel::kAllowed);
  };
  for (const GeolocationAccessLevel previous_level : all_levels) {
    for (const GeolocationAccessLevel current_level : all_levels) {
      if (previous_level == current_level) {
        // The GeolocationPrivacySwitch ensures that the previous and current
        // levels are different
        continue;
      }
      for (const bool incoming_enable : {true, false}) {
        const bool is_noop = (incoming_enable && !is_blocking(current_level)) ||
                             (!incoming_enable && is_blocking(current_level));
        const GeolocationAccessLevel expected_new_current_level =
            is_noop ? current_level
                    : (incoming_enable ? GeolocationAccessLevel::kAllowed
                                       : previous_level);
        const GeolocationAccessLevel expected_new_previous_level =
            is_noop ? previous_level : current_level;
        table.push_back(std::make_tuple(
            previous_level, current_level, incoming_enable,
            expected_new_previous_level, expected_new_current_level));
      }
    }
  }
  return table;
}

INSTANTIATE_TEST_SUITE_P(AllCombinations,
                         PrivacyHubGeolocationApplyArcLocationUpdatesTest,
                         testing::ValuesIn(GenerateTransitionTable()));

}  // namespace ash