// 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 <string_view>
#include "base/functional/bind.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/metrics/website_metrics_browser_test_mixin.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/core/device_policy_cros_test_helper.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/chromeos/reporting/metric_default_utils.h"
#include "chrome/browser/chromeos/reporting/metric_reporting_prefs.h"
#include "chrome/browser/policy/dm_token_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/ash/components/login/session/session_termination_manager.h"
#include "chromeos/dbus/missive/missive_client_test_observer.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/policy/core/common/cloud/dm_token.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/reporting/util/mock_clock.h"
#include "content/public/test/browser_test.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/cros_system_api/dbus/login_manager/dbus-constants.h"
using ::testing::AllOf;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::Le;
using ::testing::SizeIs;
using ::testing::StrEq;
namespace reporting {
namespace {
using std::literals::string_view_literals::operator""sv;
constexpr auto kTestDMToken = "token"sv;
constexpr auto kTestUrl = "https://a.example.org/"sv;
constexpr base::TimeDelta kWebsiteUsageCollectionInterval = base::Minutes(5);
constexpr base::TimeDelta kWebsiteUsageDuration = base::Minutes(2);
// Additional website usage buffer period before the browser is actually closed.
// Used when validating reported website usage data.
constexpr base::TimeDelta kWebsiteUsageBufferPeriod = base::Seconds(10);
void AssertRecordData(Priority priority, const Record& record) {
EXPECT_THAT(priority, Eq(Priority::MANUAL_BATCH));
ASSERT_TRUE(record.has_destination());
EXPECT_THAT(record.destination(), Eq(Destination::TELEMETRY_METRIC));
ASSERT_TRUE(record.has_dm_token());
EXPECT_THAT(record.dm_token(), StrEq(kTestDMToken));
ASSERT_TRUE(record.has_source_info());
EXPECT_THAT(record.source_info().source(), Eq(SourceInfo::ASH));
}
// Returns true if the record includes website usage telemetry. False otherwise.
bool IsWebsiteUsageTelemetry(const Record& record) {
MetricData record_data;
return record_data.ParseFromString(record.data()) &&
record_data.has_telemetry_data() &&
record_data.telemetry_data().has_website_telemetry() &&
record_data.telemetry_data()
.website_telemetry()
.has_website_usage_data();
}
// Browser test that validates website usage telemetry reported by the
// `WebsiteUsageTelemetrySampler` in Ash. Inheriting from
// `DevicePolicyCrosBrowserTest` enables use of `AffiliationMixin` for setting
// up profile/device affiliation.
class WebsiteUsageTelemetrySamplerBrowserTest
: public ::policy::DevicePolicyCrosBrowserTest {
protected:
WebsiteUsageTelemetrySamplerBrowserTest() {
// Initialize the mock clock.
test::MockClock::Get();
crypto_home_mixin_.MarkUserAsExisting(affiliation_mixin_.account_id());
::policy::SetDMTokenForTesting(
::policy::DMToken::CreateValidToken(std::string{kTestDMToken}));
}
void SetUpCommandLine(base::CommandLine* command_line) override {
::policy::AffiliationTestHelper::AppendCommandLineSwitchesForLoginManager(
command_line);
::policy::DevicePolicyCrosBrowserTest::SetUpCommandLine(command_line);
}
// Simulates website usage with the test URL for the specified duration.
void SimulateWebsiteUsage(base::TimeDelta running_time) {
// Create a new browser instance and open test URL in a new tab.
auto* const browser = website_metrics_browser_test_mixin_.CreateBrowser();
website_metrics_browser_test_mixin_.InsertForegroundTab(
browser, std::string{kTestUrl});
// Advance timer to simulate usage and close tab to prevent further usage
// tracking.
test::MockClock::Get().Advance(running_time);
browser->tab_strip_model()->CloseAllTabs();
// Advance timer to ensure app service reports tracked usage to the
// website usage observer.
test::MockClock::Get().Advance(kWebsiteUsageCollectionInterval);
::content::RunAllTasksUntilIdle();
}
void VerifyReportedMetricData(const MetricData& metric_data,
base::TimeDelta running_time) {
EXPECT_TRUE(metric_data.has_timestamp_ms());
const auto& website_usage_data =
metric_data.telemetry_data().website_telemetry().website_usage_data();
ASSERT_THAT(website_usage_data.website_usage(), SizeIs(1));
const auto& website_usage = website_usage_data.website_usage(0);
EXPECT_THAT(website_usage.url(), StrEq(std::string{kTestUrl}));
// There is some minor usage (usually in milliseconds) as we attempt to
// close the tab/browser and before it is actually closed, so we account for
// that below as we validate reported website usage.
const auto& max_expected_usage = running_time + kWebsiteUsageBufferPeriod;
EXPECT_THAT(website_usage.running_time_ms(),
AllOf(Ge(running_time.InMilliseconds()),
Le(max_expected_usage.InMilliseconds())));
}
void SetAllowlistedUrls(const std::vector<std::string>& allowlisted_urls) {
base::Value::List allowed_urls;
for (const auto& url : allowlisted_urls) {
allowed_urls.Append(url);
}
profile()->GetPrefs()->SetList(kReportWebsiteTelemetryAllowlist,
std::move(allowed_urls));
}
void SetAllowlistedTelemetryTypes(
const std::vector<std::string>& allowlisted_telemetry_types) {
base::Value::List allowed_telemetry_types;
for (const auto& url : allowlisted_telemetry_types) {
allowed_telemetry_types.Append(url);
}
profile()->GetPrefs()->SetList(kReportWebsiteTelemetry,
std::move(allowed_telemetry_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_};
::apps::WebsiteMetricsBrowserTestMixin website_metrics_browser_test_mixin_{
&mixin_host_};
};
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
PRE_ReportUrlUsage) {
// Set up affiliated user.
::policy::AffiliationTestHelper::PreLoginUser(
affiliation_mixin_.account_id());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
ReportUrlUsage) {
// Login as affiliated user and set policy.
::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
SetAllowlistedUrls({ContentSettingsPattern::Wildcard().ToString()});
SetAllowlistedTelemetryTypes({kWebsiteTelemetryUsageType});
// Simulate website usage and advance timer by the collection interval to
// ensure telemetry data is enqueued.
SimulateWebsiteUsage(kWebsiteUsageDuration);
::chromeos::MissiveClientTestObserver missive_observer(
base::BindRepeating(&IsWebsiteUsageTelemetry));
test::MockClock::Get().Advance(
metrics::kDefaultWebsiteTelemetryCollectionRate);
// Verify data being collected.
const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
AssertRecordData(priority, record);
MetricData metric_data;
ASSERT_TRUE(metric_data.ParseFromString(record.data()));
VerifyReportedMetricData(metric_data, kWebsiteUsageDuration);
// Ensure there is no additional usage data being reported with the next
// periodic collection.
test::MockClock::Get().Advance(
metrics::kDefaultWebsiteTelemetryCollectionRate);
EXPECT_FALSE(missive_observer.HasNewEnqueuedRecord());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
PRE_DisallowedUrlUsage) {
// Set up affiliated user.
::policy::AffiliationTestHelper::PreLoginUser(
affiliation_mixin_.account_id());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
DisallowedUrlUsage) {
// Login as affiliated user and set policy.
::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
SetAllowlistedUrls({});
SetAllowlistedTelemetryTypes({kWebsiteTelemetryUsageType});
// Simulate website usage and advance timer by the collection interval.
SimulateWebsiteUsage(kWebsiteUsageDuration);
::chromeos::MissiveClientTestObserver missive_observer(
base::BindRepeating(&IsWebsiteUsageTelemetry));
test::MockClock::Get().Advance(
metrics::kDefaultWebsiteTelemetryCollectionRate);
// Verify no telemetry data is enqueued.
EXPECT_FALSE(missive_observer.HasNewEnqueuedRecord());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
PRE_DisallowedUsageTelemetryType) {
// Set up affiliated user.
::policy::AffiliationTestHelper::PreLoginUser(
affiliation_mixin_.account_id());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
DisallowedUsageTelemetryType) {
// Login as affiliated user and set policy.
::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
SetAllowlistedUrls({ContentSettingsPattern::Wildcard().ToString()});
SetAllowlistedTelemetryTypes({});
// Simulate website usage and advance timer by the collection interval.
SimulateWebsiteUsage(kWebsiteUsageDuration);
::chromeos::MissiveClientTestObserver missive_observer(
base::BindRepeating(&IsWebsiteUsageTelemetry));
test::MockClock::Get().Advance(
metrics::kDefaultWebsiteTelemetryCollectionRate);
// Verify no telemetry data is enqueued.
EXPECT_FALSE(missive_observer.HasNewEnqueuedRecord());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
PRE_ReportSubsequentUsage) {
// Set up affiliated user.
::policy::AffiliationTestHelper::PreLoginUser(
affiliation_mixin_.account_id());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
ReportSubsequentUsage) {
// Login as affiliated user and set policy.
::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
SetAllowlistedUrls({ContentSettingsPattern::Wildcard().ToString()});
SetAllowlistedTelemetryTypes({kWebsiteTelemetryUsageType});
// Simulate website usage.
SimulateWebsiteUsage(kWebsiteUsageDuration);
SimulateWebsiteUsage(kWebsiteUsageDuration);
// Advance timer by the collection interval to ensure telemetry data is
// enqueued.
::chromeos::MissiveClientTestObserver missive_observer(
base::BindRepeating(&IsWebsiteUsageTelemetry));
test::MockClock::Get().Advance(
metrics::kDefaultWebsiteTelemetryCollectionRate);
// Verify data being collected.
const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
AssertRecordData(priority, record);
MetricData metric_data;
ASSERT_TRUE(metric_data.ParseFromString(record.data()));
VerifyReportedMetricData(metric_data,
kWebsiteUsageDuration + kWebsiteUsageDuration);
// Ensure there is no additional usage being reported with the next periodic
// collection.
test::MockClock::Get().Advance(
metrics::kDefaultWebsiteTelemetryCollectionRate);
EXPECT_FALSE(missive_observer.HasNewEnqueuedRecord());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
PRE_ReportUsageDataOnSessionTermination) {
// Set up affiliated user.
::policy::AffiliationTestHelper::PreLoginUser(
affiliation_mixin_.account_id());
}
IN_PROC_BROWSER_TEST_F(WebsiteUsageTelemetrySamplerBrowserTest,
ReportUsageDataOnSessionTermination) {
// Login as affiliated user and set policy.
::policy::AffiliationTestHelper::LoginUser(affiliation_mixin_.account_id());
SetAllowlistedUrls({ContentSettingsPattern::Wildcard().ToString()});
SetAllowlistedTelemetryTypes({kWebsiteTelemetryUsageType});
// Simulate website usage and terminate session.
SimulateWebsiteUsage(kWebsiteUsageDuration);
::chromeos::MissiveClientTestObserver missive_observer(
base::BindRepeating(&IsWebsiteUsageTelemetry));
::ash::SessionTerminationManager::Get()->StopSession(
::login_manager::SessionStopReason::USER_REQUESTS_SIGNOUT);
// Verify data being collected.
const auto [priority, record] = missive_observer.GetNextEnqueuedRecord();
AssertRecordData(priority, record);
MetricData metric_data;
ASSERT_TRUE(metric_data.ParseFromString(record.data()));
VerifyReportedMetricData(metric_data, kWebsiteUsageDuration);
}
} // namespace
} // namespace reporting