chromium/chromeos/ash/components/report/device_metrics/churn/observation_impl_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 "chromeos/ash/components/report/device_metrics/churn/observation_impl.h"

#include <memory>
#include "ash/constants/ash_features.h"
#include "base/files/file_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "chromeos/ash/components/report/device_metrics/use_case/stub_psm_client_manager.h"
#include "chromeos/ash/components/report/device_metrics/use_case/use_case.h"
#include "chromeos/ash/components/report/prefs/fresnel_pref_names.h"
#include "chromeos/ash/components/report/report_controller.h"
#include "chromeos/ash/components/report/utils/network_utils.h"
#include "chromeos/ash/components/report/utils/test_utils.h"
#include "chromeos/ash/components/system/fake_statistics_provider.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "components/version_info/channel.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace psm_rlwe = private_membership::rlwe;

namespace ash::report::device_metrics {

class ObservationImplTestBase : public testing::Test {
 public:
  ObservationImplTestBase() = default;
  ObservationImplTestBase(const ObservationImplTestBase&) = delete;
  ObservationImplTestBase& operator=(const ObservationImplTestBase&) = delete;
  ~ObservationImplTestBase() override = default;

  void SetUp() override {
    // Set the mock time to |kFakeTimeNow|.
    base::Time ts;
    ASSERT_TRUE(base::Time::FromUTCString(utils::kFakeTimeNowString, &ts));
    task_environment_.AdvanceClock(ts - base::Time::Now());

    // Register all related local state prefs.
    ReportController::RegisterPrefs(local_state_.registry());

    test_shared_loader_factory_ =
        base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
            &test_url_loader_factory_);

    system::StatisticsProvider::SetTestProvider(&statistics_provider_);
  }

 protected:
  base::Time GetFakeTimeNow() { return base::Time::Now(); }

  PrefService* GetLocalState() { return &local_state_; }

  scoped_refptr<network::SharedURLLoaderFactory> GetUrlLoaderFactory() {
    return test_shared_loader_factory_;
  }

  // Generate a well-formed fake PSM network request and response bodies for
  // testing purposes.
  const std::string GetFresnelOprfResponse() {
    FresnelPsmRlweOprfResponse psm_oprf_response;
    *psm_oprf_response.mutable_rlwe_oprf_response() =
        psm_rlwe::PrivateMembershipRlweOprfResponse();
    return psm_oprf_response.SerializeAsString();
  }

  const std::string GetFresnelQueryResponse() {
    FresnelPsmRlweQueryResponse psm_query_response;
    *psm_query_response.mutable_rlwe_query_response() =
        psm_rlwe::PrivateMembershipRlweQueryResponse();
    return psm_query_response.SerializeAsString();
  }

  void SimulateOprfRequest(
      StubPsmClientManagerDelegate* delegate,
      const psm_rlwe::PrivateMembershipRlweOprfRequest& request) {
    delegate->set_oprf_request(request);
  }

  void SimulateQueryRequest(
      StubPsmClientManagerDelegate* delegate,
      const psm_rlwe::PrivateMembershipRlweQueryRequest& request) {
    delegate->set_query_request(request);
  }

  void SimulateMembershipResponses(
      StubPsmClientManagerDelegate* delegate,
      const private_membership::rlwe::RlweMembershipResponses&
          membership_responses) {
    delegate->set_membership_responses(membership_responses);
  }

  void SimulateOprfResponse(const std::string& serialized_response_body,
                            net::HttpStatusCode response_code) {
    test_url_loader_factory_.SimulateResponseForPendingRequest(
        utils::GetOprfRequestURL().spec(), serialized_response_body,
        response_code);

    task_environment_.RunUntilIdle();
  }

  // Generate a well-formed fake PSM network response body for testing purposes.
  void SimulateQueryResponse(const std::string& serialized_response_body,
                             net::HttpStatusCode response_code) {
    test_url_loader_factory_.SimulateResponseForPendingRequest(
        utils::GetQueryRequestURL().spec(), serialized_response_body,
        response_code);

    task_environment_.RunUntilIdle();
  }

  void SimulateImportResponse(const std::string& serialized_response_body,
                              net::HttpStatusCode response_code) {
    test_url_loader_factory_.SimulateResponseForPendingRequest(
        utils::GetImportRequestURL().spec(), serialized_response_body,
        response_code);

    task_environment_.RunUntilIdle();
  }

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

 private:
  scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  TestingPrefServiceSimple local_state_;
  system::FakeStatisticsProvider statistics_provider_;
};

class ObservationImplDirectCheckInTest : public ObservationImplTestBase {
 public:
  static constexpr ChromeDeviceMetadataParameters kFakeChromeParameters = {
      version_info::Channel::STABLE /* chromeos_channel */,
      MarketSegment::MARKET_SEGMENT_CONSUMER /* market_segment */,
  };

  void SetUp() override {
    ObservationImplTestBase::SetUp();

    // |psm_client_delegate| is owned by |psm_client_manager_|.
    // Stub successful request payloads when created by the PSM client.
    std::unique_ptr<StubPsmClientManagerDelegate> psm_client_delegate =
        std::make_unique<StubPsmClientManagerDelegate>();
    SimulateOprfRequest(psm_client_delegate.get(),
                        psm_rlwe::PrivateMembershipRlweOprfRequest());
    SimulateQueryRequest(psm_client_delegate.get(),
                         psm_rlwe::PrivateMembershipRlweQueryRequest());
    SimulateMembershipResponses(psm_client_delegate.get(),
                                GetMembershipResponses());
    psm_client_manager_ =
        std::make_unique<PsmClientManager>(std::move(psm_client_delegate));

    use_case_params_ = std::make_unique<UseCaseParameters>(
        GetFakeTimeNow(), kFakeChromeParameters, GetUrlLoaderFactory(),
        utils::kFakeHighEntropySeed, GetLocalState(),
        psm_client_manager_.get());
    observation_impl_ =
        std::make_unique<ObservationImpl>(use_case_params_.get());
  }

  void TearDown() override {
    observation_impl_.reset();
    use_case_params_.reset();
    psm_client_manager_.reset();
  }

  ObservationImpl* GetObservationImpl() { return observation_impl_.get(); }

  // Returns a single positive membership response.
  psm_rlwe::RlweMembershipResponses GetMembershipResponses() {
    psm_rlwe::RlweMembershipResponses membership_responses;

    psm_rlwe::RlweMembershipResponses::MembershipResponseEntry* entry =
        membership_responses.add_membership_responses();
    private_membership::MembershipResponse* membership_response =
        entry->mutable_membership_response();
    membership_response->set_is_member(true);

    return membership_responses;
  }

  base::Time GetLastPingTimestamp() {
    return observation_impl_->GetLastPingTimestamp();
  }

  void SetLastPingTimestamp(base::Time ts) {
    observation_impl_->SetLastPingTimestamp(ts);
  }

  std::optional<FresnelImportDataRequest> GenerateImportRequestBody() {
    return observation_impl_->GenerateImportRequestBodyForTesting();
  }

 private:
  std::unique_ptr<PsmClientManager> psm_client_manager_;
  std::unique_ptr<UseCaseParameters> use_case_params_;
  std::unique_ptr<ObservationImpl> observation_impl_;
};

TEST_F(ObservationImplDirectCheckInTest, QueryFeatureFlagDisabled) {
  ASSERT_FALSE(base::FeatureList::IsEnabled(
      features::kDeviceActiveClientChurnObservationCheckMembership));
}

TEST_F(ObservationImplDirectCheckInTest, ValidateBrandNewDeviceFlow) {
  ASSERT_EQ(GetLastPingTimestamp(), base::Time::UnixEpoch());

  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 111111111111111111
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72613887;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  // Execute observation reporting logic.
  GetObservationImpl()->Run(base::DoNothing());

  // Return well formed response bodies for the pending network requests.
  SimulateImportResponse(std::string(), net::HTTP_OK);

  EXPECT_EQ(GetLastPingTimestamp(), cur_ts);
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0));
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1));
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2));
}

TEST_F(ObservationImplDirectCheckInTest,
       ValidateBrandNewDeviceFlowWithFailedCohort) {
  ASSERT_EQ(GetLastPingTimestamp(), base::Time::UnixEpoch());

  // Simulate with default cohort local state values. This indicates cohort ping
  // was not sent successfully prior to reporting observation.
  EXPECT_EQ(GetLocalState()->GetTime(
                prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp),
            base::Time::UnixEpoch());
  EXPECT_EQ(GetLocalState()->GetInteger(
                prefs::kDeviceActiveLastKnownChurnActiveStatus),
            0);

  // Execute observation reporting logic.
  GetObservationImpl()->Run(base::DoNothing());

  // Expect observation import request failed.
  EXPECT_EQ(GetLastPingTimestamp(), base::Time::UnixEpoch());
  EXPECT_FALSE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0));
  EXPECT_FALSE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1));
  EXPECT_FALSE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2));
}

TEST_F(ObservationImplDirectCheckInTest,
       GracefullyHandleImportResponseFailure) {
  ASSERT_EQ(GetLastPingTimestamp(), base::Time::UnixEpoch());

  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 111111111111111111
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72613887;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  GetObservationImpl()->Run(base::DoNothing());

  // Set invalid import response body.
  SimulateImportResponse(std::string(), net::HTTP_REQUEST_TIMEOUT);

  // Not updated since the PSM import failed via. timeout.
  EXPECT_EQ(GetLastPingTimestamp(), base::Time::UnixEpoch());
  EXPECT_FALSE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0));
  EXPECT_FALSE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1));
  EXPECT_FALSE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2));
}

TEST_F(ObservationImplDirectCheckInTest,
       ValidateRandomActiveStatusHistoryWithSuccessfulCohortPing) {
  ASSERT_EQ(GetLastPingTimestamp(), base::Time::UnixEpoch());

  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 001010010010010101
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72393877;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  // Execute observation reporting logic.
  GetObservationImpl()->Run(base::DoNothing());

  // Return well formed response bodies for the pending network requests.
  SimulateImportResponse(std::string(), net::HTTP_OK);

  EXPECT_EQ(GetLastPingTimestamp(), cur_ts);
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0));
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1));
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2));
}

TEST_F(ObservationImplDirectCheckInTest, ValidateNewDeviceChurnMetadata) {
  ASSERT_EQ(GetLastPingTimestamp(), base::Time::UnixEpoch());

  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 001010010010010101
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72393877;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  // Execute observation reporting logic.
  GetObservationImpl()->Run(base::DoNothing());

  // Return well formed response bodies for the pending network requests.
  SimulateImportResponse(std::string(), net::HTTP_OK);

  EXPECT_EQ(GetLastPingTimestamp(), cur_ts);
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0));
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1));
  EXPECT_TRUE(GetLocalState()->GetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2));
}

TEST_F(ObservationImplDirectCheckInTest, GenerateImportRequestBody_Successful) {
  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 001010010010010101
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72393877;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  std::optional<FresnelImportDataRequest> import_request =
      GenerateImportRequestBody();
  ASSERT_TRUE(import_request.has_value());
  EXPECT_EQ(import_request.value().import_data_size(), 3);
}

TEST_F(ObservationImplDirectCheckInTest, GenerateImportRequestBody) {
  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 001010010010010101
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72393877;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  // Field under test.
  std::optional<FresnelImportDataRequest> import_request;

  // Failure to generate import request body.
  GetLocalState()->SetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0, true);
  GetLocalState()->SetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1, true);
  GetLocalState()->SetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2, true);
  import_request = GenerateImportRequestBody();
  EXPECT_EQ(import_request->import_data_size(), 0);

  // Generates period 0 observation.
  GetLocalState()->SetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0, false);
  import_request = GenerateImportRequestBody();
  EXPECT_EQ(import_request->import_data_size(), 1);

  // Generates period 0 and 1 observations.
  GetLocalState()->SetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1, false);
  import_request = GenerateImportRequestBody();
  EXPECT_EQ(import_request->import_data_size(), 2);

  // Generates period 0, 1, and 2 observations.
  GetLocalState()->SetBoolean(
      prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2, false);
  import_request = GenerateImportRequestBody();
  EXPECT_EQ(import_request->import_data_size(), 3);
}

TEST_F(ObservationImplDirectCheckInTest, GenerateObservationImportData) {
  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 001010010010010101
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72393877;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  // Generates period 0, 1, and 2 observations.
  std::optional<FresnelImportDataRequest> import_request =
      GenerateImportRequestBody();
  ASSERT_EQ(import_request->import_data_size(), 3);

  struct {
    int period;
    bool expected_monthly_active_status;
    bool expected_yearly_active_status;
    const std::string expected_first_active_week;
    const std::string expected_last_powerwash_week;
  } kTestCases[] = {
      {0, false, true, "UNKNOWN", "UNKNOWN"},
      {1, true, false, "UNKNOWN", "UNKNOWN"},
      {2, false, true, "UNKNOWN", "UNKNOWN"},
  };

  // Validate observation import data.
  FresnelImportData obs_data;
  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(testing::Message() << "Period: " << test_case.period);
    obs_data = import_request->import_data(test_case.period);
    ASSERT_TRUE(obs_data.has_churn_observation_metadata());

    EXPECT_EQ(obs_data.churn_observation_metadata().monthly_active_status(),
              test_case.expected_monthly_active_status);
    EXPECT_EQ(obs_data.churn_observation_metadata().yearly_active_status(),
              test_case.expected_yearly_active_status);
    EXPECT_EQ(obs_data.churn_observation_metadata().first_active_week(),
              test_case.expected_first_active_week);
    EXPECT_EQ(obs_data.churn_observation_metadata().last_powerwash_week(),
              test_case.expected_last_powerwash_week);
  }
}

TEST_F(ObservationImplDirectCheckInTest, ObservationImportDataNewDeviceChurn) {
  // Validate metadata when new device churn feature is enabled.
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {features::kDeviceActiveClientChurnObservationNewDeviceMetadata},
      /*disabled_features*/ {});

  // Observation import will only go through if cohort imported successfully.
  // Setup initial value to be last active in Jan-2023.
  // Value represents device was active each of the 18 months prior.
  // Represents binary: 0100010100 001010010010010101
  base::Time cur_ts = GetFakeTimeNow();
  int cur_value = 72393877;
  GetLocalState()->SetTime(prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp,
                           cur_ts);
  GetLocalState()->SetInteger(prefs::kDeviceActiveLastKnownChurnActiveStatus,
                              cur_value);

  struct {
    const std::string activate_date_vpd;
    std::string first_active_week_obs_0;
    std::string first_active_week_obs_1;
    std::string first_active_week_obs_2;
  } kTestCases[] = {
      {"2023-01", "2023-01", "2023-01", "2023-01"},
      {"2022-36", "2022-36", "2022-36", "2022-36"},

      /* ActivateDate is > 4 months old */
      {"2022-35", "", "", ""},
      /* ActivateDate is undefined */
      {std::string(), "UNKNOWN", "UNKNOWN", "UNKNOWN"},
      /* ActivateDate is incorrectly formatted */
      {"123-456", "UNKNOWN", "UNKNOWN", "UNKNOWN"},
  };

  // Initialize fake statistics provider to test various activate date inputs.
  system::FakeStatisticsProvider statistics_provider;
  system::StatisticsProvider::SetTestProvider(&statistics_provider);

  // Validate if new device churn metadata is attached in observation ping.
  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(testing::Message() << "Test input Activate Date VPD as: "
                                    << test_case.activate_date_vpd);

    // Set ActivateDate field in FakeStatisticsProvider before reading
    // ActivateDate in the GenerateImportRequestBody method.
    statistics_provider.SetMachineStatistic(system::kActivateDateKey,
                                            test_case.activate_date_vpd);

    // Method under test
    std::optional<FresnelImportDataRequest> import_request =
        GenerateImportRequestBody();
    ASSERT_EQ(import_request->import_data_size(), 3);

    auto obs_data_0 = import_request->import_data(0);
    auto obs_data_1 = import_request->import_data(1);
    auto obs_data_2 = import_request->import_data(2);

    ASSERT_TRUE(obs_data_0.has_churn_observation_metadata());
    ASSERT_TRUE(obs_data_1.has_churn_observation_metadata());
    ASSERT_TRUE(obs_data_2.has_churn_observation_metadata());

    EXPECT_EQ(obs_data_0.churn_observation_metadata().first_active_week(),
              test_case.first_active_week_obs_0);
    EXPECT_EQ(obs_data_1.churn_observation_metadata().first_active_week(),
              test_case.first_active_week_obs_1);
    EXPECT_EQ(obs_data_2.churn_observation_metadata().first_active_week(),
              test_case.first_active_week_obs_2);
  }

  // Set statistics test provider to nullptr after tests.
  system::StatisticsProvider::SetTestProvider(nullptr);
}

}  // namespace ash::report::device_metrics