chromium/components/variations/variations_seed_store_unittest.cc

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

#include "components/variations/variations_seed_store.h"

#include <memory>
#include <utility>

#include "base/base64.h"
#include "base/build_time.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/protobuf_matchers.h"
#include "base/test/scoped_command_line.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/version.h"
#include "build/build_config.h"
#include "components/prefs/testing_pref_service.h"
#include "components/variations/client_filterable_state.h"
#include "components/variations/pref_names.h"
#include "components/variations/proto/study.pb.h"
#include "components/variations/proto/variations_seed.pb.h"
#include "components/variations/variations_safe_seed_store_local_state.h"
#include "components/variations/variations_switches.h"
#include "components/variations/variations_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zlib/google/compression_utils.h"

#if BUILDFLAG(IS_ANDROID)
#include "components/variations/android/variations_seed_bridge.h"
#endif  // BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chromeos/ash/components/dbus/featured/fake_featured_client.h"
#include "chromeos/ash/components/dbus/featured/featured.pb.h"
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

namespace variations {
namespace {

EqualsProto;

// The sentinel value that may be stored as the latest variations seed value in
// prefs to indicate that the latest seed is identical to the safe seed.
// Note: This constant is intentionally duplicated in the test because it is
// persisted to disk. In order to maintain backward-compatibility, it's
// important that code continue to correctly handle this specific constant, even
// if the constant used internally in the implementation changes.
constexpr char kIdenticalToSafeSeedSentinel[] =;

// TODO(crbug.com/40764723): Consider consolidating TestVariationsSeedStore and
// SignatureVerifyingVariationsSeedStore. Outside of tests, signature
// verification is enabled although prior to crrev.com/c/2181564, signature
// verification was not done on iOS or Android.
class TestVariationsSeedStore : public VariationsSeedStore {};

class SignatureVerifyingVariationsSeedStore : public VariationsSeedStore {};

// Creates a base::Time object from the corresponding raw value. The specific
// implementation is not important; it's only important that distinct inputs map
// to distinct outputs.
base::Time WrapTime(int64_t time) {}

// Populates |seed| with simple test data. The resulting seed will contain one
// study called "test", which contains one experiment called "abc" with
// probability weight 100. |seed|'s study field will be cleared before adding
// the new study.
VariationsSeed CreateTestSeed() {}

// Returns a ClientFilterableState with all fields set to "interesting" values
// for testing.
std::unique_ptr<ClientFilterableState> CreateTestClientFilterableState() {}

// Serializes |seed| to protobuf binary format.
std::string SerializeSeed(const VariationsSeed& seed) {}

// Compresses |data| using Gzip compression and returns the result.
std::string Gzip(const std::string& data) {}

// Gzips |data| and then base64-encodes it.
std::string GzipAndBase64Encode(const std::string& data) {}

// Serializes |seed| to gzipped base64-encoded protobuf binary format.
std::string SerializeSeedBase64(const VariationsSeed& seed) {}

// Wrapper over base::Base64Decode() that returns the result.
std::string Base64DecodeData(const std::string& data) {}

// Sample seeds and the server produced delta between them to verify that the
// client code is able to decode the deltas produced by the server.
struct {} kSeedDeltaTestData;

// Sets all seed-related prefs to non-default values. Used to verify whether
// pref values were cleared.
void SetAllSeedPrefsToNonDefaultValues(PrefService* prefs) {}

// Checks whether the given pref has its default value in |prefs|.
bool PrefHasDefaultValue(const TestingPrefServiceSimple& prefs,
                         const char* pref_name) {}

void CheckRegularSeedPrefsAreSet(const TestingPrefServiceSimple& prefs) {}

void CheckRegularSeedPrefsAreCleared(const TestingPrefServiceSimple& prefs) {}

void CheckSafeSeedPrefsAreSet(const TestingPrefServiceSimple& prefs) {}

void CheckSafeSeedPrefsAreCleared(const TestingPrefServiceSimple& prefs) {}

}  // namespace

TEST(VariationsSeedStoreTest, LoadSeed_ValidSeed) {}

TEST(VariationsSeedStoreTest, LoadSeed_InvalidSeed) {}

TEST(VariationsSeedStoreTest, LoadSeed_InvalidSignature) {}

TEST(VariationsSeedStoreTest, LoadSeed_InvalidProto) {}

TEST(VariationsSeedStoreTest, LoadSeed_RejectEmptySignature) {}

TEST(VariationsSeedStoreTest, LoadSeed_AcceptEmptySignature) {}

TEST(VariationsSeedStoreTest, LoadSeed_EmptySeed) {}

TEST(VariationsSeedStoreTest, LoadSeed_IdenticalToSafeSeed) {}

TEST(VariationsSeedStoreTest, ApplyDeltaPatch) {}

class VariationsStoreSeedDataTest : public ::testing::Test,
                                    public ::testing::WithParamInterface<bool> {};

INSTANTIATE_TEST_SUITE_P();

TEST_P(VariationsStoreSeedDataTest, StoreSeedData) {}

TEST_P(VariationsStoreSeedDataTest, ParsedSeed) {}

TEST_P(VariationsStoreSeedDataTest, CountryCode) {}

TEST_P(VariationsStoreSeedDataTest, GzippedSeed) {}

TEST_P(VariationsStoreSeedDataTest, GzippedEmptySeed) {}

TEST_P(VariationsStoreSeedDataTest, DeltaCompressed) {}

TEST_P(VariationsStoreSeedDataTest, DeltaCompressedGzipped) {}

TEST_P(VariationsStoreSeedDataTest, DeltaButNoInitialSeed) {}

TEST_P(VariationsStoreSeedDataTest, BadDelta) {}

TEST_P(VariationsStoreSeedDataTest, IdenticalToSafeSeed) {}

// Verifies that the cached serial number is correctly updated when a new seed
// is saved.
TEST_P(VariationsStoreSeedDataTest,
       GetLatestSerialNumber_UpdatedWithNewStoredSeed) {}

TEST(VariationsSeedStoreTest, LoadSafeSeed_ValidSeed) {}

TEST(VariationsSeedStoreTest, LoadSafeSeed_CorruptSeed) {}

TEST(VariationsSeedStoreTest, LoadSafeSeed_InvalidSignature) {}

TEST(VariationsSeedStoreTest, LoadSafeSeed_EmptySeed) {}

struct InvalidSafeSeedTestParams {};

StoreInvalidSafeSeedTest;

INSTANTIATE_TEST_SUITE_P();

// Verify that attempting to store an invalid safe seed fails and does not
// modify Local State's existing safe-seed-related prefs.
TEST_P(StoreInvalidSafeSeedTest, StoreSafeSeed) {}

TEST(VariationsSeedStoreTest, StoreSafeSeed_ValidSignature) {}

TEST(VariationsSeedStoreTest, StoreSafeSeed_IdenticalToLatestSeed) {}

TEST(VariationsSeedStoreTest, StoreSafeSeed_PreviouslyIdenticalToLatestSeed) {}

TEST(VariationsSeedStoreTest, VerifySeedSignature) {}

TEST(VariationsSeedStoreTest, LastFetchTime_DistinctSeeds) {}

TEST(VariationsSeedStoreTest, LastFetchTime_IdenticalSeeds) {}

TEST(VariationsSeedStoreTest, GetLatestSerialNumber_LoadsInitialValue) {}

TEST(VariationsSeedStoreTest, GetLatestSerialNumber_EmptyWhenNoSeedIsSaved) {}

TEST(VariationsSeedStoreTest, GetLatestSerialNumber_ClearsPrefsOnFailure) {}

// Verifies that GetTimeForStudyDateChecks() returns the server timestamp for
// when the regular seed was fetched,|kVariationsSeedDate|, when the time is
// more recent than the build time.
TEST(VariationsSeedStoreTest, RegularSeedTimeReturned) {}

// Verifies that GetTimeForStudyDateChecks() returns the server timestamp for
// when the safe seed was fetched, |kVariationsSafeSeedDate|, when the time is
// more recent than the build time.
TEST(VariationsSeedStoreTest, SafeSeedTimeReturned) {}

// Verifies that GetTimeForStudyDateChecks() returns the build time when it is
// more recent than |kVariationsSeedDate|.
TEST(VariationsSeedStoreTest, BuildTimeReturnedForRegularSeed) {}

// Verifies that GetTimeForStudyDateChecks() returns the build time when it is
// more recent than |kVariationsSafeSeedDate|.
TEST(VariationsSeedStoreTest, BuildTimeReturnedForSafeSeed) {}

// Verifies that GetTimeForStudyDateChecks() returns the build time when the
// seed time is null.
TEST(VariationsSeedStoreTest, BuildTimeReturnedForNullSeedTimes) {}

#if BUILDFLAG(IS_ANDROID)
TEST(VariationsSeedStoreTest, ImportFirstRunJavaSeed) {
  const std::string test_seed_data = "raw_seed_data_test";
  const std::string test_seed_signature = "seed_signature_test";
  const std::string test_seed_country = "seed_country_code_test";
  const int64_t test_response_date = 1234567890;
  const bool test_is_gzip_compressed = true;
  android::SetJavaFirstRunPrefsForTesting(test_seed_data, test_seed_signature,
                                          test_seed_country, test_response_date,
                                          test_is_gzip_compressed);

  auto seed = android::GetVariationsFirstRunSeed();
  EXPECT_EQ(test_seed_data, seed->data);
  EXPECT_EQ(test_seed_signature, seed->signature);
  EXPECT_EQ(test_seed_country, seed->country);
  EXPECT_EQ(test_response_date, seed->date.InMillisecondsSinceUnixEpoch());
  EXPECT_EQ(test_is_gzip_compressed, seed->is_gzip_compressed);

  android::ClearJavaFirstRunPrefs();
  seed = android::GetVariationsFirstRunSeed();
  EXPECT_EQ("", seed->data);
  EXPECT_EQ("", seed->signature);
  EXPECT_EQ("", seed->country);
  EXPECT_EQ(0, seed->date.InMillisecondsSinceUnixEpoch());
  EXPECT_FALSE(seed->is_gzip_compressed);
}

class VariationsSeedStoreFirstRunPrefsTest
    : public ::testing::TestWithParam<bool> {};

INSTANTIATE_TEST_SUITE_P(VariationsSeedStoreTest,
                         VariationsSeedStoreFirstRunPrefsTest,
                         ::testing::Bool());

TEST_P(VariationsSeedStoreFirstRunPrefsTest, FirstRunPrefsAllowed) {
  bool use_first_run_prefs = GetParam();

  const std::string test_seed_data = "raw_seed_data_test";
  const std::string test_seed_signature = "seed_signature_test";
  const std::string test_seed_country = "seed_country_code_test";
  const int64_t test_response_date = 1234567890;
  const bool test_is_gzip_compressed = true;
  android::SetJavaFirstRunPrefsForTesting(test_seed_data, test_seed_signature,
                                          test_seed_country, test_response_date,
                                          test_is_gzip_compressed);

  const VariationsSeed test_seed = CreateTestSeed();
  const std::string seed_data = SerializeSeed(test_seed);
  const std::string base64_seed_data = SerializeSeedBase64(test_seed);
  auto seed = std::make_unique<SeedResponse>();
  seed->data = seed_data;
  seed->signature = "java_seed_signature";
  seed->country = "java_seed_country";
  seed->date = base::Time::FromMillisecondsSinceUnixEpoch(test_response_date) +
               base::Days(1);
  seed->is_gzip_compressed = false;

  TestingPrefServiceSimple prefs;
  VariationsSeedStore::RegisterPrefs(prefs.registry());
  TestVariationsSeedStore seed_store(&prefs, /*initial_seed=*/std::move(seed),
                                     use_first_run_prefs);

  seed = android::GetVariationsFirstRunSeed();

  // VariationsSeedStore must not modify Java prefs at all.
  EXPECT_EQ(test_seed_data, seed->data);
  EXPECT_EQ(test_seed_signature, seed->signature);
  EXPECT_EQ(test_seed_country, seed->country);
  EXPECT_EQ(test_response_date, seed->date.InMillisecondsSinceUnixEpoch());
  EXPECT_EQ(test_is_gzip_compressed, seed->is_gzip_compressed);
  if (use_first_run_prefs) {
    EXPECT_TRUE(android::HasMarkedPrefsForTesting());
  } else {
    EXPECT_FALSE(android::HasMarkedPrefsForTesting());
  }

  // Seed should be stored in prefs.
  EXPECT_FALSE(PrefHasDefaultValue(prefs, prefs::kVariationsCompressedSeed));
  EXPECT_EQ(base64_seed_data,
            prefs.GetString(prefs::kVariationsCompressedSeed));
}
#endif  // BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_CHROMEOS_ASH)
const featured::SeedDetails CreateDummySafeSeed(
    ClientFilterableState* client_state,
    base::Time fetch_time_to_store) {
  featured::SeedDetails expected_seed;
  expected_seed.set_b64_compressed_data(kTestSeedData.base64_compressed_data);
  expected_seed.set_signature(kTestSeedData.base64_signature);
  expected_seed.set_milestone(92);
  expected_seed.set_locale(client_state->locale);
  expected_seed.set_date(
      client_state->reference_date.ToDeltaSinceWindowsEpoch().InMilliseconds());
  expected_seed.set_permanent_consistency_country(
      client_state->permanent_consistency_country);
  expected_seed.set_session_consistency_country(
      client_state->session_consistency_country);
  expected_seed.set_fetch_time(
      fetch_time_to_store.ToDeltaSinceWindowsEpoch().InMilliseconds());
  return expected_seed;
}

// Checks that |platform_data| and |expected_data| deserialize to the same
// VariationsSeed proto.
// |platform_data| and |expected_data| are base64_compressed forms of seed data.
void ExpectSeedData(const std::string& platform_data,
                    const std::string& expected_data) {
  std::string decoded_platform_data;
  EXPECT_TRUE(base::Base64Decode(platform_data, &decoded_platform_data));
  std::string uncompressed_decoded_platform_data;
  EXPECT_TRUE(compression::GzipUncompress(decoded_platform_data,
                                          &uncompressed_decoded_platform_data));
  VariationsSeed platform_seed;
  EXPECT_TRUE(
      platform_seed.ParseFromString(uncompressed_decoded_platform_data));

  std::string decoded_expected_data;
  EXPECT_TRUE(base::Base64Decode(expected_data, &decoded_expected_data));
  std::string uncompressed_decoded_expected_data;
  EXPECT_TRUE(compression::GzipUncompress(decoded_expected_data,
                                          &uncompressed_decoded_expected_data));
  VariationsSeed expected_seed;
  EXPECT_TRUE(
      expected_seed.ParseFromString(uncompressed_decoded_expected_data));

  EXPECT_THAT(platform_seed, EqualsProto(expected_seed));
}

// Manually verifying each field in featured::SeedDetails rather than using
// EqualsProto is necessary because the
// featured::SeedDetails::b64_compressed_data field may be different between
// |platform| and |expected| even if the data unserializes to the same
// VariationsSeed. This could be caused by implementation differences between
// different versions of compression::GzipCompress.
//
// To accurately compare two featured::SeedDetails protos, the
// `b64_compressed_data` should be deserialized into a VariationsSeed proto and
// the two VariationsSeed protos should be compared.
void ExpectSafeSeed(const featured::SeedDetails& platform,
                    const featured::SeedDetails expected) {
  ExpectSeedData(platform.b64_compressed_data(),
                 expected.b64_compressed_data());
  EXPECT_EQ(platform.locale(), expected.locale());
  EXPECT_EQ(platform.milestone(), expected.milestone());
  EXPECT_EQ(platform.permanent_consistency_country(),
            expected.permanent_consistency_country());
  EXPECT_EQ(platform.session_consistency_country(),
            expected.session_consistency_country());
  EXPECT_EQ(platform.signature(), expected.signature());
  EXPECT_EQ(platform.date(), expected.date());
  EXPECT_EQ(platform.fetch_time(), expected.fetch_time());
}

TEST(VariationsSeedStoreTest, SendSafeSeedToPlatform_SucceedFirstAttempt) {
  TestingPrefServiceSimple prefs;
  VariationsSeedStore::RegisterPrefs(prefs.registry());
  SignatureVerifyingVariationsSeedStore seed_store(&prefs);

  ash::featured::FeaturedClient::InitializeFake();
  ash::featured::FakeFeaturedClient* client =
      ash::featured::FakeFeaturedClient::Get();
  client->AddResponse(true);

  std::unique_ptr<ClientFilterableState> client_state =
      CreateDummyClientFilterableState();
  base::Time now = base::Time::Now();
  const base::Time fetch_time_to_store = now - base::Hours(1);
  featured::SeedDetails expected_platform_seed =
      CreateDummySafeSeed(client_state.get(), fetch_time_to_store);
  std::string expected_seed_data;
  ASSERT_TRUE(base::Base64Decode(kTestSeedData.base64_uncompressed_data,
                                 &expected_seed_data));

  // Verify that storing the safe seed succeeded.
  EXPECT_TRUE(seed_store.StoreSafeSeed(
      expected_seed_data, expected_platform_seed.signature(),
      expected_platform_seed.milestone(), *client_state, fetch_time_to_store));

  // Verify that the validated safe seed was received on Platform.
  ExpectSafeSeed(client->latest_safe_seed(), expected_platform_seed);
  EXPECT_EQ(client->handle_seed_fetched_attempts(), 1);

  ash::featured::FeaturedClient::Shutdown();
}

TEST(VariationsSeedStoreTest, SendSafeSeedToPlatform_FailFirstAttempt) {
  TestingPrefServiceSimple prefs;
  VariationsSeedStore::RegisterPrefs(prefs.registry());
  SignatureVerifyingVariationsSeedStore seed_store(&prefs);

  ash::featured::FeaturedClient::InitializeFake();
  ash::featured::FakeFeaturedClient* client =
      ash::featured::FakeFeaturedClient::Get();
  client->AddResponse(false);
  client->AddResponse(true);

  std::unique_ptr<ClientFilterableState> client_state =
      CreateDummyClientFilterableState();
  base::Time now = base::Time::Now();
  const base::Time fetch_time_to_store = now - base::Hours(1);
  featured::SeedDetails expected_platform_seed =
      CreateDummySafeSeed(client_state.get(), fetch_time_to_store);
  std::string expected_seed_data;
  ASSERT_TRUE(base::Base64Decode(kTestSeedData.base64_uncompressed_data,
                                 &expected_seed_data));

  // Verify that storing the safe seed succeeded.
  EXPECT_TRUE(seed_store.StoreSafeSeed(
      expected_seed_data, expected_platform_seed.signature(),
      expected_platform_seed.milestone(), *client_state, fetch_time_to_store));

  // Verify that the validated safe seed was received on Platform.
  ExpectSafeSeed(client->latest_safe_seed(), expected_platform_seed);
  EXPECT_EQ(client->handle_seed_fetched_attempts(), 2);

  ash::featured::FeaturedClient::Shutdown();
}

TEST(VariationsSeedStoreTest, SendSafeSeedToPlatform_FailTwoAttempts) {
  TestingPrefServiceSimple prefs;
  VariationsSeedStore::RegisterPrefs(prefs.registry());
  SignatureVerifyingVariationsSeedStore seed_store(&prefs);

  ash::featured::FeaturedClient::InitializeFake();
  ash::featured::FakeFeaturedClient* client =
      ash::featured::FakeFeaturedClient::Get();
  client->AddResponse(false);
  client->AddResponse(false);

  std::unique_ptr<ClientFilterableState> client_state =
      CreateDummyClientFilterableState();
  base::Time now = base::Time::Now();
  const base::Time fetch_time_to_store = now - base::Hours(1);
  featured::SeedDetails seed =
      CreateDummySafeSeed(client_state.get(), fetch_time_to_store);
  std::string seed_data;
  ASSERT_TRUE(
      base::Base64Decode(kTestSeedData.base64_uncompressed_data, &seed_data));

  // Verify that storing the safe seed succeeded.
  EXPECT_TRUE(seed_store.StoreSafeSeed(seed_data, seed.signature(),
                                       seed.milestone(), *client_state,
                                       fetch_time_to_store));

  // Verify that the validated safe seed was not received on Platform.
  featured::SeedDetails empty_seed;
  EXPECT_THAT(client->latest_safe_seed(), EqualsProto(empty_seed));
  EXPECT_EQ(client->handle_seed_fetched_attempts(), 2);

  ash::featured::FeaturedClient::Shutdown();
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

}  // namespace variations