chromium/chrome/browser/ash/policy/reporting/metrics_reporting/apps/app_events_observer_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 <memory>
#include <string>
#include <vector>

#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_ash.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/login/test/cryptohome_mixin.h"
#include "chrome/browser/ash/policy/affiliation/affiliation_mixin.h"
#include "chrome/browser/ash/policy/affiliation/affiliation_test_helper.h"
#include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h"
#include "chrome/browser/ash/policy/reporting/metrics_reporting/metric_reporting_manager.h"
#include "chrome/browser/ash/policy/reporting/metrics_reporting/metric_reporting_prefs.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/policy/dm_token_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom-shared.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/user_display_mode.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/dbus/missive/missive_client_test_observer.h"
#include "components/prefs/pref_service.h"
#include "components/reporting/proto/synced/metric_data.pb.h"
#include "components/reporting/proto/synced/record.pb.h"
#include "components/reporting/proto/synced/record_constants.pb.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/features.h"
#include "components/services/app_service/public/protos/app_types.pb.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_launcher.h"
#include "content/public/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-shared.h"
#include "url/gurl.h"

using ::testing::Contains;
using ::testing::Eq;
using ::testing::SizeIs;
using ::testing::StrEq;

namespace reporting {
namespace {

// Test DM token used to associate reported events.
constexpr char kDMToken[] = "token";

// Standalone webapp start URL.
constexpr char kWebAppUrl[] = "https://test.example.com/";

void AssertRecordData(Priority priority, const Record& record) {
  EXPECT_THAT(priority, Eq(Priority::SLOW_BATCH));
  ASSERT_TRUE(record.has_destination());
  EXPECT_THAT(record.destination(), Eq(Destination::EVENT_METRIC));
  ASSERT_TRUE(record.has_dm_token());
  EXPECT_THAT(record.dm_token(), StrEq(kDMToken));
  ASSERT_TRUE(record.has_source_info());
  EXPECT_THAT(record.source_info().source(), Eq(SourceInfo::ASH));
}

void AssertMetricData(const MetricData& metric_data) {
  EXPECT_TRUE(metric_data.has_timestamp_ms());
  EXPECT_TRUE(metric_data.has_event_data());
  ASSERT_TRUE(metric_data.has_telemetry_data());
  EXPECT_TRUE(metric_data.telemetry_data().has_app_telemetry());
}

// Returns true if the record includes the specified metric event type. False
// otherwise.
bool IsMetricEventOfType(MetricEventType metric_event_type,
                         const Record& record) {
  MetricData record_data;
  return record_data.ParseFromString(record.data()) &&
         record_data.has_event_data() &&
         (record_data.event_data().type() == metric_event_type);
}

// Browser test that validates events collected and reported by the
// `AppEventsObserver`. Inheriting from `DevicePolicyCrosBrowserTest` enables
// use of `AffiliationMixin` for setting up profile/device affiliation. Only
// available in Ash.
class AppEventsObserverBrowserTest
    : public ::policy::DevicePolicyCrosBrowserTest {
 protected:
  AppEventsObserverBrowserTest()
      : scoped_feature_list_(kEnableAppEventsObserver) {
    crypto_home_mixin_.MarkUserAsExisting(affiliation_mixin_.account_id());
    ::policy::SetDMTokenForTesting(
        ::policy::DMToken::CreateValidToken(kDMToken));
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ::policy::AffiliationTestHelper::AppendCommandLineSwitchesForLoginManager(
        command_line);
    ::policy::DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line);
  }

  // Helper that installs a standalone webapp with the specified start url.
  ::webapps::AppId InstallStandaloneWebApp(const GURL& start_url) {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
    web_app_info->scope = start_url.GetWithoutFilename();
    web_app_info->display_mode = ::blink::mojom::DisplayMode::kStandalone;
    web_app_info->user_display_mode =
        ::web_app::mojom::UserDisplayMode::kStandalone;
    return ::web_app::test::InstallWebApp(profile(), std::move(web_app_info));
  }

  // Helper that uninstalls the standalone webapp with the specified app id.
  void UninstallStandaloneWebApp(const ::webapps::AppId& app_id) {
    ::apps::AppServiceProxyFactory::GetForProfile(profile())->UninstallSilently(
        app_id, ::apps::UninstallSource::kAppList);
  }

  void SetAllowedAppReportingTypes(const std::vector<std::string>& app_types) {
    base::Value::List allowed_app_types;
    for (const auto& app_type : app_types) {
      allowed_app_types.Append(app_type);
    }
    profile()->GetPrefs()->SetList(::ash::reporting::kReportAppInventory,
                                   std::move(allowed_app_types));
  }

  Profile* profile() const {
    return ash::ProfileHelper::Get()->GetProfileByAccountId(
        affiliation_mixin_.account_id());
  }

  ::policy::DevicePolicyCrosTestHelper test_helper_;
  ::policy::AffiliationMixin affiliation_mixin_{&mixin_host_, &test_helper_};
  ::ash::CryptohomeMixin crypto_home_mixin_{&mixin_host_};
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest, PRE_ReportInstalledApp) {
  // Set up affiliated user.
  ::policy::AffiliationTestHelper::PreLoginUser(
      affiliation_mixin_.account_id());
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest, ReportInstalledApp) {
  // Login as affiliated user and set policy.
  ::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryPWA});

  ::chromeos::MissiveClientTestObserver missive_observer(base::BindRepeating(
      &IsMetricEventOfType, MetricEventType::APP_INSTALLED));
  const auto app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
  const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
  AssertRecordData(priority, record);
  MetricData metric_data;
  ASSERT_TRUE(metric_data.ParseFromString(record.data()));
  AssertMetricData(metric_data);
  ASSERT_TRUE(
      metric_data.telemetry_data().app_telemetry().has_app_install_data());
  const auto& app_install_data =
      metric_data.telemetry_data().app_telemetry().app_install_data();
  EXPECT_THAT(app_install_data.app_id(), StrEq(kWebAppUrl));
  EXPECT_THAT(app_install_data.app_type(),
              Eq(::apps::ApplicationType::APPLICATION_TYPE_WEB));
  EXPECT_THAT(
      app_install_data.app_install_source(),
      Eq(::apps::ApplicationInstallSource::APPLICATION_INSTALL_SOURCE_BROWSER));
  EXPECT_THAT(
      app_install_data.app_install_reason(),
      Eq(::apps::ApplicationInstallReason::APPLICATION_INSTALL_REASON_SYNC));
  EXPECT_THAT(
      app_install_data.app_install_time(),
      Eq(::apps::ApplicationInstallTime::APPLICATION_INSTALL_TIME_RUNNING));
  EXPECT_THAT(profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
              Contains(app_id).Times(1));
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest,
                       PRE_PRE_ReportPreinstalledApp) {
  // Set up affiliated user.
  ::policy::AffiliationTestHelper::PreLoginUser(
      affiliation_mixin_.account_id());
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest,
                       PRE_ReportPreinstalledApp) {
  // Login as affiliated user and install app before closing the session.
  ::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
  const auto app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
    ASSERT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(app_id).Times(1));
  ::ash::Shell::Get()->session_controller()->RequestSignOut();
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest, ReportPreinstalledApp) {
  ::chromeos::MissiveClientTestObserver missive_observer(base::BindRepeating(
      &IsMetricEventOfType, MetricEventType::APP_INSTALLED));
  ::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryPWA});
    ASSERT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        SizeIs(1));

  const auto app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
  ::content::RunAllTasksUntilIdle();
  ASSERT_FALSE(missive_observer.HasNewEnqueuedRecord());
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(app_id).Times(1));
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest, PRE_ReportLaunchedApp) {
  // Set up affiliated user.
  ::policy::AffiliationTestHelper::PreLoginUser(
      affiliation_mixin_.account_id());
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest, ReportLaunchedApp) {
  // Login as affiliated user and set policy.
  ::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryPWA});

  const auto app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));
  ::chromeos::MissiveClientTestObserver missive_observer(
      base::BindRepeating(&IsMetricEventOfType, MetricEventType::APP_LAUNCHED));
  ::web_app::LaunchWebAppBrowser(profile(), app_id);
  const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
  AssertRecordData(priority, record);
  MetricData metric_data;
  ASSERT_TRUE(metric_data.ParseFromString(record.data()));
  AssertMetricData(metric_data);
  ASSERT_TRUE(
      metric_data.telemetry_data().app_telemetry().has_app_launch_data());
  const auto& app_launch_data =
      metric_data.telemetry_data().app_telemetry().app_launch_data();
  EXPECT_THAT(
      app_launch_data.app_launch_source(),
      Eq(::apps::ApplicationLaunchSource::APPLICATION_LAUNCH_SOURCE_TEST));
  EXPECT_THAT(app_launch_data.app_id(), StrEq(kWebAppUrl));
  EXPECT_THAT(app_launch_data.app_type(),
              Eq(::apps::ApplicationType::APPLICATION_TYPE_WEB));
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest, PRE_ReportUninstalledApp) {
  // Set up affiliated user.
  ::policy::AffiliationTestHelper::PreLoginUser(
      affiliation_mixin_.account_id());
}

IN_PROC_BROWSER_TEST_F(AppEventsObserverBrowserTest, ReportUninstalledApp) {
  // Login as affiliated user and set policy.
  ::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
  SetAllowedAppReportingTypes({::ash::reporting::kAppCategoryPWA});

  const auto app_id = InstallStandaloneWebApp(GURL(kWebAppUrl));

    ASSERT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(app_id).Times(1));

  ::chromeos::MissiveClientTestObserver missive_observer(base::BindRepeating(
      &IsMetricEventOfType, MetricEventType::APP_UNINSTALLED));
  UninstallStandaloneWebApp(app_id);
  const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
  AssertRecordData(priority, record);
  MetricData metric_data;
  ASSERT_TRUE(metric_data.ParseFromString(record.data()));
  AssertMetricData(metric_data);
  ASSERT_TRUE(
      metric_data.telemetry_data().app_telemetry().has_app_uninstall_data());
  const auto& app_uninstall_data =
      metric_data.telemetry_data().app_telemetry().app_uninstall_data();
  EXPECT_THAT(app_uninstall_data.app_uninstall_source(),
              Eq(::apps::ApplicationUninstallSource::
                     APPLICATION_UNINSTALL_SOURCE_APP_LIST));
  EXPECT_THAT(app_uninstall_data.app_id(), StrEq(kWebAppUrl));
  EXPECT_THAT(app_uninstall_data.app_type(),
              Eq(::apps::ApplicationType::APPLICATION_TYPE_WEB));
    EXPECT_THAT(
        profile()->GetPrefs()->GetList(::ash::reporting::kAppsInstalled),
        Contains(app_id).Times(0));
}

}  // namespace
}  // namespace reporting