chromium/chrome/browser/metrics/structured/metadata_processor_ash_browsertest.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 "chrome/browser/metrics/structured/metadata_processor_ash.h"

#include <optional>
#include <utility>

#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/login_screen_test_api.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "chrome/browser/ash/app_mode/kiosk_test_helper.h"
#include "chrome/browser/ash/app_mode/web_app/web_kiosk_app_data.h"
#include "chrome/browser/ash/app_mode/web_app/web_kiosk_app_manager.h"
#include "chrome/browser/ash/login/app_mode/test/kiosk_test_helpers.h"
#include "chrome/browser/ash/login/demo_mode/demo_mode_test_utils.h"
#include "chrome/browser/ash/login/demo_mode/demo_session.h"
#include "chrome/browser/ash/login/existing_user_controller.h"
#include "chrome/browser/ash/login/test/logged_in_user_mixin.h"
#include "chrome/browser/ash/login/test/session_manager_state_waiter.h"
#include "chrome/browser/ash/login/wizard_controller.h"
#include "chrome/browser/ash/ownership/fake_owner_settings_service.h"
#include "chrome/browser/ash/policy/core/device_local_account.h"
#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
#include "chrome/browser/ash/policy/test_support/embedded_policy_test_server_mixin.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part_ash.h"
#include "chrome/browser/metrics/structured/test/structured_metrics_mixin.h"
#include "chrome/test/base/fake_gaia_mixin.h"
#include "chromeos/ash/components/dbus/session_manager/fake_session_manager_client.h"
#include "components/metrics/metrics_service.h"
#include "components/metrics/structured/structured_events.h"
#include "components/metrics/structured/structured_metrics_client.h"
#include "components/metrics/structured/structured_metrics_features.h"
#include "components/metrics/structured/structured_metrics_service.h"
#include "components/policy/core/common/cloud/cloud_policy_constants.h"
#include "components/policy/core/common/cloud/mock_cloud_policy_store.h"
#include "components/policy/core/common/cloud/test/policy_builder.h"
#include "components/policy/core/common/device_local_account_type.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"

namespace {
namespace em = enterprise_management;
using ash::KioskSessionInitializedWaiter;
using ash::LoginScreenTestApi;
using ash::ScopedDeviceSettings;
using ash::WebKioskAppManager;
using testing::InvokeWithoutArgs;
using StructuredEventProto = metrics::StructuredEventProto;
using StructuredDataProto = metrics::StructuredDataProto;
using PrimaryUserSegment = StructuredEventProto::PrimaryUserSegment;
using DeviceSegment = StructuredDataProto::DeviceSegment;

const char kAccountId1[] = "[email protected]";
const char kDisplayName1[] = "display name 1";
const char kAppInstallUrl[] = "https://app.com/install";

// The name hash of "CrOSEvents".
constexpr uint64_t kCrosEventsHash = UINT64_C(12657197978410187837);

// The name hash of "chrome::CrOSEvents::NoMetricsEvent".
constexpr uint64_t kNoMetricsEventHash = UINT64_C(5106854608989380457);

std::optional<em::PolicyData::MarketSegment> GetMarketSegment(
    DeviceSegment device_segment) {
  switch (device_segment) {
    case StructuredDataProto::UNKNOWN:
    case StructuredDataProto::CONSUMER:
      return std::nullopt;
    case StructuredDataProto::EDUCATION:
      return em::PolicyData::ENROLLED_EDUCATION;
    case StructuredDataProto::ENTERPRISE:
      return em::PolicyData::ENROLLED_ENTERPRISE;
  }
  NOTREACHED_IN_MIGRATION();
  return std::nullopt;
}

std::optional<em::PolicyData::MetricsLogSegment> GetMetricsLogSegment(
    PrimaryUserSegment primary_user_segment) {
  switch (primary_user_segment) {
    case StructuredEventProto::K12:
      return em::PolicyData::K12;
    case StructuredEventProto::UNIVERSITY:
      return em::PolicyData::UNIVERSITY;
    case StructuredEventProto::NON_PROFIT:
      return em::PolicyData::NONPROFIT;
    case StructuredEventProto::ENTERPRISE_ORGANIZATION:
      return em::PolicyData::ENTERPRISE;
    case StructuredEventProto::UNMANAGED:
    case StructuredEventProto::KIOS_APP:
    case StructuredEventProto::MANAGED_GUEST_SESSION:
    case StructuredEventProto::DEMO_MODE:
    case StructuredEventProto::UNKNOWN_PRIMARY_USER_TYPE:
      return std::nullopt;
  }
  NOTREACHED_IN_MIGRATION();
  return std::nullopt;
}

}  // namespace

namespace metrics::structured {

namespace {
class TestCase {
 public:
  TestCase(PrimaryUserSegment primary_user_segment,
           DeviceSegment device_segment)
      : primary_user_segment_(primary_user_segment),
        device_segment_(device_segment) {}

  PrimaryUserSegment GetPrimaryUserSegement() const {
    return primary_user_segment_;
  }

  DeviceSegment GetDeviceSegment() const { return device_segment_; }

  std::optional<em::PolicyData::MetricsLogSegment> GetMetricsLogSegment()
      const {
    return ::GetMetricsLogSegment(primary_user_segment_);
  }

  std::optional<em::PolicyData::MarketSegment> GetMarketSegment() const {
    return ::GetMarketSegment(device_segment_);
  }

  bool IsPublicSession() const {
    return primary_user_segment_ == StructuredEventProto::MANAGED_GUEST_SESSION;
  }

  bool IsKioskApp() const {
    return primary_user_segment_ == StructuredEventProto::KIOS_APP;
  }

  bool IsDemoSession() const {
    return primary_user_segment_ == StructuredEventProto::DEMO_MODE;
  }

 private:
  PrimaryUserSegment primary_user_segment_;
  DeviceSegment device_segment_;
};

TestCase UserCase(PrimaryUserSegment user_segment,
                  DeviceSegment device_segment) {
  TestCase test_case(user_segment, device_segment);
  return test_case;
}

TestCase MgsCase(DeviceSegment device_segment) {
  TestCase test_case(StructuredEventProto::MANAGED_GUEST_SESSION,
                     device_segment);
  return test_case;
}

TestCase KioskCase(DeviceSegment device_segment) {
  TestCase test_case(StructuredEventProto::KIOS_APP, device_segment);
  return test_case;
}

TestCase DemoModeCase() {
  TestCase test_case(StructuredEventProto::DEMO_MODE,
                     StructuredDataProto::ENTERPRISE);
  return test_case;
}
}  // namespace

class MetadataProcessorTest : public policy::DevicePolicyCrosBrowserTest,
                              public testing::WithParamInterface<TestCase> {
 public:
  MetadataProcessorTest() {
    if (GetParam().IsDemoSession()) {
      device_state_.SetState(
          ash::DeviceStateMixin::State::OOBE_COMPLETED_DEMO_MODE);
    }
  }

  void SetUpInProcessBrowserTestFixture() override {
    policy::DevicePolicyCrosBrowserTest::SetUpInProcessBrowserTestFixture();
    InitializePolicy();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(ash::switches::kOobeSkipPostLogin);
    DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line);
  }

  void TearDownOnMainThread() override {
    settings_.reset();
    policy::DevicePolicyCrosBrowserTest::TearDownOnMainThread();
  }

 protected:
  void InitializePolicy() {
    device_policy()->policy_data().set_public_key_version(1);
    policy::DeviceLocalAccountTestHelper::SetupDeviceLocalAccount(
        &device_local_account_policy_, kAccountId1, kDisplayName1);
  }

  void BuildDeviceLocalAccountPolicy() {
    device_local_account_policy_.SetDefaultSigningKey();
    device_local_account_policy_.Build();
  }

  void UploadDeviceLocalAccountPolicy() {
    BuildDeviceLocalAccountPolicy();
    logged_in_user_mixin_.GetEmbeddedPolicyTestServerMixin()
        ->UpdateExternalPolicy(
            policy::dm_protocol::kChromePublicAccountPolicyType, kAccountId1,
            device_local_account_policy_.payload().SerializeAsString());
  }

  void UploadAndInstallDeviceLocalAccountPolicy() {
    UploadDeviceLocalAccountPolicy();
    session_manager_client()->set_device_local_account_policy(
        kAccountId1, device_local_account_policy_.GetBlob());
  }

  void SetUp() override {
    // These tests are only applicable if structured metrics service is enabled.
    if (!base::FeatureList::IsEnabled(kEnabledStructuredMetricsService)) {
      GTEST_SKIP() << "Skipping test: Structured Metrics Service and CrOS "
                      "Events must be enabled";
    }
    policy::DevicePolicyCrosBrowserTest::SetUp();
  }

  void SetDevicePolicy() {
    UploadAndInstallDeviceLocalAccountPolicy();
    // Add an account with DeviceLocalAccountType::kPublicSession.
    AddPublicSessionToDevicePolicy(kAccountId1);

    std::optional<em::PolicyData::MarketSegment> market_segment =
        GetParam().GetMarketSegment();
    if (market_segment) {
      device_policy()->policy_data().set_market_segment(market_segment.value());
      RefreshDevicePolicy();
    }
    WaitForPolicy();
  }

  void AddPublicSessionToDevicePolicy(const std::string& username) {
    em::ChromeDeviceSettingsProto& proto(device_policy()->payload());
    policy::DeviceLocalAccountTestHelper::AddPublicSession(&proto, username);
    RefreshDevicePolicy();
    logged_in_user_mixin_.GetEmbeddedPolicyTestServerMixin()
        ->UpdateDevicePolicy(proto);
  }

  void WaitForDisplayName(const std::string& user_id,
                          const std::string& expected_display_name) {
    policy::DictionaryLocalStateValueWaiter("UserDisplayName",
                                            expected_display_name, user_id)
        .Wait();
  }

  void WaitForPolicy() {
    // Wait for the display name becoming available as that indicates
    // device-local account policy is fully loaded, which is a prerequisite for
    // successful login.
    WaitForDisplayName(account_id_1_.GetUserEmail(), kDisplayName1);
  }

  void LogInUser() {
    std::optional<em::PolicyData::MetricsLogSegment> log_segment =
        GetParam().GetMetricsLogSegment();
    if (log_segment) {
      logged_in_user_mixin_.GetEmbeddedPolicyTestServerMixin()
          ->SetMetricsLogSegment(log_segment.value());
    }
    logged_in_user_mixin_.LogInUser();
  }

  void StartPublicSession() {
    StartPublicSessionLogin();
    WaitForSessionStart();
  }

  void StartPublicSessionLogin() {
    // Start login into the device-local account.
    auto* host = ash::LoginDisplayHost::default_host();
    ASSERT_TRUE(host);
    host->StartSignInScreen();
    auto* controller = ash::ExistingUserController::current_controller();
    ASSERT_TRUE(controller);

    ash::UserContext user_context(user_manager::UserType::kPublicAccount,
                                  account_id_1_);
    controller->Login(user_context, ash::SigninSpecifics());
  }

  void StartDemoSession() {
    // Set Demo Mode config to online.
    ash::DemoSession::SetDemoConfigForTesting(
        ash::DemoSession::DemoModeConfig::kOnline);
    ash::test::LockDemoDeviceInstallAttributes();
    ash::DemoSession::StartIfInDemoMode();

    // Start the public session, Demo Mode is a special public session.
    StartPublicSession();
  }

  void PrepareAppLaunch() {
    std::vector<policy::DeviceLocalAccount> device_local_accounts = {
        policy::DeviceLocalAccount(
            policy::DeviceLocalAccount::EphemeralMode::kUnset,
            policy::WebKioskAppBasicInfo(kAppInstallUrl, "", ""),
            kAppInstallUrl)};

    settings_ = std::make_unique<ScopedDeviceSettings>();
    int ui_update_count = LoginScreenTestApi::GetUiUpdateCount();
    policy::SetDeviceLocalAccountsForTesting(
        settings_->owner_settings_service(), device_local_accounts);
    // Wait for the Kiosk App configuration to reload.
    LoginScreenTestApi::WaitForUiUpdate(ui_update_count);
  }

  bool LaunchApp() {
    return LoginScreenTestApi::LaunchApp(
        WebKioskAppManager::Get()->GetAppByAccountId(account_id_2_)->app_id());
  }

  void StartKioskApp() {
    PrepareAppLaunch();
    LaunchApp();
    KioskSessionInitializedWaiter().Wait();
  }

  void WaitForSessionStart() {
    if (IsSessionStarted()) {
      return;
    }
    ash::test::WaitForPrimaryUserSessionStart();
  }

  bool IsSessionStarted() {
    return session_manager::SessionManager::Get()->IsSessionStarted();
  }

  void Wait() { base::RunLoop().RunUntilIdle(); }

 protected:
  StructuredMetricsMixin structured_metrics_mixin_{&mixin_host_,
                                                   /*setup_profile=*/false};

  // Helper function to find an event in an already build Uma Proto.
  static std::optional<StructuredEventProto> FindEvent(
      const StructuredDataProto& structured_data,
      uint64_t project_name_hash,
      uint64_t event_name_hash) {
    for (const auto& event : structured_data.events()) {
      if (event.project_name_hash() == project_name_hash &&
          event.event_name_hash() == event_name_hash) {
        return event;
      }
    }
    return std::nullopt;
  }

 private:
  ash::LoggedInUserMixin logged_in_user_mixin_{
      &mixin_host_, /*test_base=*/this, embedded_test_server(),
      ash::LoggedInUserMixin::LogInType::kManaged};
  policy::UserPolicyBuilder device_local_account_policy_;

  const AccountId account_id_1_ =
      AccountId::FromUserEmail(policy::GenerateDeviceLocalAccountUserId(
          kAccountId1,
          policy::DeviceLocalAccountType::kPublicSession));
  const AccountId account_id_2_ =
      AccountId::FromUserEmail(policy::GenerateDeviceLocalAccountUserId(
          kAppInstallUrl,
          policy::DeviceLocalAccountType::kWebKioskApp));
  // Not strictly necessary, but makes kiosk tests run much faster.
  base::AutoReset<bool> skip_splash_wait_override_ =
      ash::KioskTestHelper::SkipSplashScreenWait();
  std::unique_ptr<ScopedDeviceSettings> settings_;
};

// TODO(b/303139902) re-enable test once flakiness is addressed.
IN_PROC_BROWSER_TEST_P(MetadataProcessorTest, DISABLED_UserMetadata) {
  using NoMetricsEvent = events::v2::cr_os_events::NoMetricsEvent;

  SetDevicePolicy();

  if (GetParam().IsPublicSession()) {
    StartPublicSession();
  } else if (GetParam().IsKioskApp()) {
    StartKioskApp();
  } else if (GetParam().IsDemoSession()) {
    StartDemoSession();
  } else {
    LogInUser();
  }

  structured_metrics_mixin_.UpdateRecordingState(true);

  structured_metrics_mixin_.WaitUntilKeysReady();

  ASSERT_TRUE(structured_metrics_mixin_.GetRecorder()->CanProvideMetrics());

  StructuredMetricsClient::Record(std::move(NoMetricsEvent()));

  Wait();

  structured_metrics_mixin_.WaitUntilEventRecorded(kCrosEventsHash,
                                                   kNoMetricsEventHash);

  structured_metrics_mixin_.GetService()->Flush(
      metrics::MetricsLogsEventManager::CreateReason::kUnknown);

  std::unique_ptr<ChromeUserMetricsExtension> uma_proto =
      structured_metrics_mixin_.GetUmaProto();
  ASSERT_NE(uma_proto.get(), nullptr);

  const StructuredDataProto& structured_data = uma_proto->structured_data();
  EXPECT_EQ(structured_data.device_segment(), GetParam().GetDeviceSegment());

  std::optional<StructuredEventProto> event =
      FindEvent(structured_data, kCrosEventsHash, kNoMetricsEventHash);
  ASSERT_TRUE(event.has_value());

  EXPECT_EQ(event->event_sequence_metadata().primary_user_segment(),
            GetParam().GetPrimaryUserSegement());
}

INSTANTIATE_TEST_SUITE_P(
    ,
    MetadataProcessorTest,
    testing::Values(
        UserCase(StructuredEventProto::UNMANAGED,
                 StructuredDataProto::CONSUMER),
        UserCase(StructuredEventProto::NON_PROFIT,
                 StructuredDataProto::CONSUMER),
        UserCase(StructuredEventProto::UNIVERSITY,
                 StructuredDataProto::EDUCATION),
        UserCase(StructuredEventProto::NON_PROFIT,
                 StructuredDataProto::EDUCATION),
        UserCase(StructuredEventProto::K12, StructuredDataProto::ENTERPRISE),
        UserCase(StructuredEventProto::ENTERPRISE_ORGANIZATION,
                 StructuredDataProto::ENTERPRISE),
        metrics::structured::KioskCase(StructuredDataProto::ENTERPRISE),
        MgsCase(StructuredDataProto::CONSUMER),
        DemoModeCase()));

}  // namespace metrics::structured