blob: 82165a74fc38f302acc6a5afc9c17ee1d49d1dc2 [file] [log] [blame]
// Copyright 2024 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/seed_reader_writer.h"
#include "base/base64.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_entropy_provider.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/thread.h"
#include "base/timer/mock_timer.h"
#include "base/version_info/channel.h"
#include "components/prefs/testing_pref_service.h"
#include "components/variations/pref_names.h"
#include "components/variations/variations_seed_store.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"
namespace variations {
namespace {
using ::testing::IsEmpty;
using ::testing::TestWithParam;
using ::testing::Values;
const base::FilePath::CharType kSeedFilename[] = FILE_PATH_LITERAL("TestSeed");
// Used for clients that do not participate in SeedFiles experiment.
constexpr char kNoGroup[] = "";
// Compresses `data` using Gzip compression.
std::string Gzip(const std::string& data) {
std::string compressed;
CHECK(compression::GzipCompress(data, &compressed));
return compressed;
}
// Creates and serializes a test VariationsSeed.
std::string CreateVariationsSeed() {
VariationsSeed seed;
seed.add_study()->set_name("TestStudy");
std::string serialized_seed;
seed.SerializeToString(&serialized_seed);
return serialized_seed;
}
// Creates, serializes, and then Gzip compresses a test seed.
std::string CreateCompressedVariationsSeed() {
return Gzip(CreateVariationsSeed());
}
struct SeedReaderWriterTestParams {
using TupleT =
std::tuple<SeedFieldsPrefs, std::string_view, version_info::Channel>;
SeedReaderWriterTestParams(SeedFieldsPrefs seed_fields_prefs,
std::string_view field_trial_group,
version_info::Channel channel)
: seed_fields_prefs(seed_fields_prefs),
field_trial_group(field_trial_group),
channel(channel) {}
explicit SeedReaderWriterTestParams(const TupleT& t)
: SeedReaderWriterTestParams(std::get<0>(t),
std::get<1>(t),
std::get<2>(t)) {}
SeedFieldsPrefs seed_fields_prefs;
std::string_view field_trial_group;
version_info::Channel channel;
};
struct ExpectedFieldTrialGroupTestParams {
using TupleT = std::tuple<SeedFieldsPrefs, version_info::Channel>;
ExpectedFieldTrialGroupTestParams(
variations::SeedFieldsPrefs seed_fields_prefs,
version_info::Channel channel)
: seed_fields_prefs(seed_fields_prefs), channel(channel) {}
explicit ExpectedFieldTrialGroupTestParams(const TupleT& t)
: ExpectedFieldTrialGroupTestParams(std::get<0>(t), std::get<1>(t)) {}
variations::SeedFieldsPrefs seed_fields_prefs;
version_info::Channel channel;
};
class SeedReaderWriterTestBase {
public:
SeedReaderWriterTestBase()
: file_writer_thread_("SeedReaderWriter Test thread"),
entropy_providers_(std::make_unique<const MockEntropyProviders>(
MockEntropyProviders::Results{.low_entropy =
kAlwaysUseLastGroup})) {
VariationsSeedStore::RegisterPrefs(local_state_.registry());
scoped_feature_list_.InitWithEmptyFeatureAndFieldTrialLists();
file_writer_thread_.Start();
CHECK(temp_dir_.CreateUniqueTempDir());
temp_seed_file_path_ = temp_dir_.GetPath().Append(kSeedFilename);
}
~SeedReaderWriterTestBase() = default;
protected:
base::test::ScopedFeatureList scoped_feature_list_;
base::test::TaskEnvironment task_environment_;
base::FilePath temp_seed_file_path_;
base::Thread file_writer_thread_;
base::ScopedTempDir temp_dir_;
TestingPrefServiceSimple local_state_;
base::MockOneShotTimer timer_;
std::unique_ptr<const MockEntropyProviders> entropy_providers_;
};
class ExpectedFieldTrialGroupChannelsTest
: public SeedReaderWriterTestBase,
public TestWithParam<ExpectedFieldTrialGroupTestParams> {};
class ExpectedFieldTrialGroupAllChannelsTest
: public ExpectedFieldTrialGroupChannelsTest {};
class ExpectedFieldTrialGroupPreStableTest
: public ExpectedFieldTrialGroupChannelsTest {};
class ExpectedFieldTrialGroupStableTest
: public SeedReaderWriterTestBase,
public TestWithParam<SeedFieldsPrefs> {};
class ExpectedFieldTrialGroupUnknownTest
: public SeedReaderWriterTestBase,
public TestWithParam<SeedFieldsPrefs> {};
INSTANTIATE_TEST_SUITE_P(
All,
ExpectedFieldTrialGroupAllChannelsTest,
::testing::ConvertGenerator<ExpectedFieldTrialGroupTestParams::TupleT>(
::testing::Combine(::testing::Values(kRegularSeedFieldsPrefs,
kSafeSeedFieldsPrefs),
::testing::Values(version_info::Channel::UNKNOWN,
version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA,
version_info::Channel::STABLE))));
// If empty seed file dir given, client is not assigned a group.
TEST_P(ExpectedFieldTrialGroupAllChannelsTest, NoSeedFileDir) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/base::FilePath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial), IsEmpty());
}
// If no entropy provider given, client is not assigned a group.
TEST_P(ExpectedFieldTrialGroupAllChannelsTest, NoEntropyProvider) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
/*entropy_providers=*/nullptr, file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial), IsEmpty());
}
INSTANTIATE_TEST_SUITE_P(
All,
ExpectedFieldTrialGroupPreStableTest,
::testing::ConvertGenerator<ExpectedFieldTrialGroupTestParams::TupleT>(
::testing::Combine(::testing::Values(kRegularSeedFieldsPrefs,
kSafeSeedFieldsPrefs),
::testing::Values(version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA))));
// If channel is pre-stable, client is assigned a group.
TEST_P(ExpectedFieldTrialGroupPreStableTest, PreStable) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial),
::testing::AnyOf(kControlGroup, kSeedFilesGroup));
}
INSTANTIATE_TEST_SUITE_P(All,
ExpectedFieldTrialGroupStableTest,
::testing::Values(kRegularSeedFieldsPrefs,
kSafeSeedFieldsPrefs));
// If channel is stable, trial has been registered.
TEST_P(ExpectedFieldTrialGroupStableTest, Stable) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam(), version_info::Channel::STABLE, entropy_providers_.get(),
file_writer_thread_.task_runner());
EXPECT_TRUE(base::FieldTrialList::TrialExists(kSeedFileTrial));
}
INSTANTIATE_TEST_SUITE_P(All,
ExpectedFieldTrialGroupUnknownTest,
::testing::Values(kRegularSeedFieldsPrefs,
kSafeSeedFieldsPrefs));
// If channel is unknown, client is not assigned a group.
TEST_P(ExpectedFieldTrialGroupUnknownTest, Unknown) {
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam(), version_info::Channel::UNKNOWN, entropy_providers_.get(),
file_writer_thread_.task_runner());
EXPECT_THAT(base::FieldTrialList::FindFullName(kSeedFileTrial), IsEmpty());
}
class SeedReaderWriterGroupTest
: public SeedReaderWriterTestBase,
public TestWithParam<SeedReaderWriterTestParams> {
public:
SeedReaderWriterGroupTest() {
SetUpSeedFileTrial(std::string(GetParam().field_trial_group));
}
};
class SeedReaderWriterSeedFilesGroupTest : public SeedReaderWriterGroupTest {};
class SeedReaderWriterLocalStateGroupsTest : public SeedReaderWriterGroupTest {
};
// Verifies clients in SeedFiles group write seeds to a seed file.
TEST_P(SeedReaderWriterSeedFilesGroupTest, WriteSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string seed_data = CreateVariationsSeed();
const base::Time seed_date = base::Time::Now();
const base::Time fetch_time = base::Time::Now();
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = seed_data,
.signature = "signature",
.milestone = 2,
.seed_date = seed_date,
.client_fetch_time = fetch_time,
.session_country_code = "us",
});
// Force write.
timer_.Fire();
file_writer_thread_.FlushForTesting();
// Verify that a seed was written to a seed file.
const std::string compressed_seed = Gzip(seed_data);
const std::string base64_compressed_seed =
base::Base64Encode(compressed_seed);
std::string seed_file_data;
ASSERT_TRUE(base::ReadFileToString(temp_seed_file_path_, &seed_file_data));
EXPECT_EQ(seed_file_data, compressed_seed);
// Verify that the seed data is loaded correctly.
EXPECT_EQ(seed_reader_writer.GetSeedData().storage_format,
StoredSeed::StorageFormat::kCompressed);
EXPECT_EQ(seed_reader_writer.GetSeedData().data, compressed_seed);
EXPECT_EQ(seed_reader_writer.GetSeedData().signature, "signature");
EXPECT_EQ(seed_reader_writer.GetSeedData().milestone, 2);
EXPECT_EQ(seed_reader_writer.GetSeedData().seed_date, seed_date);
EXPECT_EQ(seed_reader_writer.GetSeedData().client_fetch_time, fetch_time);
}
// Verifies that a seed is cleared from a seed file for clients in the SeedFiles
// group.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ClearSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed in a seed file.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, compressed_seed));
// Store other fields in local state prefs.
local_state_.SetString(GetParam().seed_fields_prefs.signature, "signature");
local_state_.SetInteger(GetParam().seed_fields_prefs.milestone, 92);
local_state_.SetTime(GetParam().seed_fields_prefs.seed_date,
base::Time::Now());
local_state_.SetString(GetParam().seed_fields_prefs.session_country_code,
"us");
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Verify seed was loaded correctly.
ASSERT_THAT(seed_reader_writer.GetSeedData().data, Not(IsEmpty()));
ASSERT_THAT(seed_reader_writer.GetSeedData().signature, Not(IsEmpty()));
ASSERT_NE(seed_reader_writer.GetSeedData().milestone, 0);
ASSERT_FALSE(seed_reader_writer.GetSeedData().seed_date.is_null());
ASSERT_THAT(seed_reader_writer.GetSeedData().session_country_code,
Not(IsEmpty()));
// Clear seed and force write.
seed_reader_writer.ClearSeedInfo();
timer_.Fire();
file_writer_thread_.FlushForTesting();
// Verify seed cleared correctly in a seed file.
// File should be empty.
std::string seed_file_data;
ASSERT_TRUE(base::ReadFileToString(temp_seed_file_path_, &seed_file_data));
EXPECT_THAT(seed_file_data, IsEmpty());
// Returned seed data should be empty.
EXPECT_THAT(seed_reader_writer.GetSeedData().data, IsEmpty());
EXPECT_THAT(seed_reader_writer.GetSeedData().signature, IsEmpty());
EXPECT_EQ(seed_reader_writer.GetSeedData().milestone, 0);
EXPECT_TRUE(seed_reader_writer.GetSeedData().seed_date.is_null());
// Session country code is not cleared.
EXPECT_THAT(seed_reader_writer.GetSeedData().session_country_code,
Not(IsEmpty()));
// Local state prefs should be cleared.
EXPECT_THAT(local_state_.GetString(GetParam().seed_fields_prefs.seed),
IsEmpty());
EXPECT_THAT(local_state_.GetString(GetParam().seed_fields_prefs.signature),
IsEmpty());
EXPECT_EQ(local_state_.GetInteger(GetParam().seed_fields_prefs.milestone), 0);
EXPECT_EQ(local_state_.GetTime(GetParam().seed_fields_prefs.seed_date),
base::Time());
// Session country code is not cleared.
EXPECT_THAT(
local_state_.GetString(GetParam().seed_fields_prefs.session_country_code),
Not(IsEmpty()));
}
// Verifies that session country code is cleared from a seed file for clients in
// the SeedFiles group.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ClearSessionCountryCode) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
local_state_.SetString(GetParam().seed_fields_prefs.session_country_code,
"us");
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
ASSERT_THAT(seed_reader_writer.GetSeedData().session_country_code,
Not(IsEmpty()));
seed_reader_writer.ClearSessionCountry();
// Session country code is cleared.
EXPECT_THAT(seed_reader_writer.GetSeedData().session_country_code, IsEmpty());
// Local state pref should be cleared.
EXPECT_THAT(
local_state_.GetString(GetParam().seed_fields_prefs.session_country_code),
IsEmpty());
}
// Verifies clients in SeedFiles group read seeds from the seed file.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadSeedFileBasedSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, compressed_seed));
const std::string_view seed_data_field = GetParam().seed_fields_prefs.seed;
local_state_.SetString(seed_data_field, "unused seed");
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Ensure seed data loaded from seed file.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ(compressed_seed, seed_reader_writer.GetSeedData().data);
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(seed_data_field, "Safe") ? "Safe" : "Latest"}),
/*sample=*/1, /*expected_bucket_count=*/1);
}
// Verifies clients in SeedFiles group do not crash if reading empty seed file.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadEmptySeedFile) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, ""));
const std::string_view seed_data_field = GetParam().seed_fields_prefs.seed;
local_state_.SetString(seed_data_field, "unused seed");
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(seed_data_field, "Safe") ? "Safe" : "Latest"}),
/*sample=*/1, /*expected_bucket_count=*/1);
// Ensure seed data loaded from seed file.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ("", seed_reader_writer.GetSeedData().data);
}
// Verifies clients in SeedFiles group read seeds from local state prefs if no
// seed file found.
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadMissingSeedFile) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
const std::string_view seed_data_field = GetParam().seed_fields_prefs.seed;
local_state_.SetString(seed_data_field, base::Base64Encode(compressed_seed));
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Ensure read failed due to seed file not existing.
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(seed_data_field, "Safe") ? "Safe" : "Latest"}),
/*sample=*/0, /*expected_bucket_count=*/1);
// Ensure seed data from local state prefs is loaded and decoded.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ(compressed_seed, seed_reader_writer.GetSeedData().data);
}
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadMissingSeedFileEmptyLocalState) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string_view seed_data_field = GetParam().seed_fields_prefs.seed;
local_state_.ClearPref(seed_data_field);
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Ensure read failed due to seed file not existing.
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(seed_data_field, "Safe") ? "Safe" : "Latest"}),
/*sample=*/0, /*expected_bucket_count=*/1);
// Ensure seed data from local state prefs is loaded and decoded.
EXPECT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
EXPECT_THAT(seed_reader_writer.GetSeedData().data, IsEmpty());
}
TEST_P(SeedReaderWriterSeedFilesGroupTest,
ReadMissingSeedFileEmptyCorruptGzip) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
std::string compressed_seed = CreateCompressedVariationsSeed();
const std::string_view seed_data_field = GetParam().seed_fields_prefs.seed;
compressed_seed[5] ^= 0xFF;
compressed_seed[10] ^= 0xFF;
local_state_.SetString(seed_data_field, base::Base64Encode(compressed_seed));
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Ensure read failed due to seed file not existing.
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(seed_data_field, "Safe") ? "Safe" : "Latest"}),
/*sample=*/0, /*expected_bucket_count=*/1);
// Ensure seed data from local state prefs is loaded and decoded.
EXPECT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
EXPECT_THAT(seed_reader_writer.GetSeedData().data, IsEmpty());
}
TEST_P(SeedReaderWriterSeedFilesGroupTest,
ReadMissingSeedFileEmptyInvalidBase64) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
const std::string_view seed_data_field = GetParam().seed_fields_prefs.seed;
local_state_.SetString(seed_data_field, "invalid base64");
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Ensure read failed due to seed file not existing.
histogram_tester.ExpectUniqueSample(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(seed_data_field, "Safe") ? "Safe" : "Latest"}),
/*sample=*/0, /*expected_bucket_count=*/1);
// Ensure seed data from local state prefs is loaded and decoded.
EXPECT_EQ(StoredSeed::StorageFormat::kCompressed,
seed_reader_writer.GetSeedData().storage_format);
EXPECT_THAT(seed_reader_writer.GetSeedData().data, IsEmpty());
}
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadSeedData) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string seed_data = CreateVariationsSeed();
const std::string signature = "completely valid signature";
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = seed_data,
.signature = signature,
});
std::string read_seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&read_seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kSuccess);
EXPECT_EQ(read_seed_data, seed_data);
EXPECT_EQ(base64_seed_signature, signature);
}
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadSeedDataCorruptGzip) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
std::string compressed_seed = CreateCompressedVariationsSeed();
compressed_seed[5] ^= 0xFF;
compressed_seed[10] ^= 0xFF;
seed_reader_writer.StoreRawSeedForTesting(compressed_seed);
std::string seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kCorruptGzip);
}
TEST_P(SeedReaderWriterSeedFilesGroupTest, ReadSeedDataExceedsSizeLimit) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// 51MiB of uncompressed data to exceed 50MiB limit.
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = std::string(51 * 1024 * 1024, 'A'),
.signature = "ignored signature",
});
std::string seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kExceedsUncompressedSizeLimit);
}
INSTANTIATE_TEST_SUITE_P(
All,
SeedReaderWriterSeedFilesGroupTest,
::testing::ConvertGenerator<SeedReaderWriterTestParams::TupleT>(
::testing::Combine(::testing::Values(kRegularSeedFieldsPrefs,
kSafeSeedFieldsPrefs),
::testing::Values(kSeedFilesGroup),
::testing::Values(version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA))));
// Verifies clients using local state to store seeds write seeds to Local State.
TEST_P(SeedReaderWriterLocalStateGroupsTest, WriteSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string seed_data = CreateVariationsSeed();
const base::Time seed_date = base::Time::Now();
const base::Time fetch_time = base::Time::Now();
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = seed_data,
.signature = "signature",
.milestone = 2,
.seed_date = seed_date,
.client_fetch_time = fetch_time,
.session_country_code = "us",
});
// Ensure there's no pending write.
EXPECT_FALSE(timer_.IsRunning());
// Verify seed stored correctly, should only be found in Local State prefs.
EXPECT_FALSE(base::PathExists(temp_seed_file_path_));
const std::string compressed_seed = Gzip(seed_data);
const std::string base64_compressed_seed =
base::Base64Encode(compressed_seed);
EXPECT_EQ(local_state_.GetString(GetParam().seed_fields_prefs.seed),
base64_compressed_seed);
EXPECT_EQ(local_state_.GetString(GetParam().seed_fields_prefs.signature),
"signature");
EXPECT_EQ(local_state_.GetInteger(GetParam().seed_fields_prefs.milestone), 2);
EXPECT_EQ(local_state_.GetTime(GetParam().seed_fields_prefs.seed_date),
seed_date);
EXPECT_EQ(local_state_.GetTime(GetParam().seed_fields_prefs.client_fetch_time),
fetch_time);
}
// Verifies that a seed is cleared from Local State and that seed file is
// deleted if present for clients using local state to store seeds.
TEST_P(SeedReaderWriterLocalStateGroupsTest, ClearSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Create and store seed.
const std::string compressed_seed = CreateCompressedVariationsSeed();
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, compressed_seed));
local_state_.SetString(GetParam().seed_fields_prefs.seed,
base::Base64Encode(compressed_seed));
local_state_.SetString(GetParam().seed_fields_prefs.signature, "signature");
local_state_.SetInteger(GetParam().seed_fields_prefs.milestone, 92);
local_state_.SetTime(GetParam().seed_fields_prefs.seed_date,
base::Time::Now());
local_state_.SetString(GetParam().seed_fields_prefs.session_country_code,
"us");
// Clear seed and force file delete.
seed_reader_writer.ClearSeedInfo();
file_writer_thread_.FlushForTesting();
// Returned seed data should be empty.
EXPECT_THAT(seed_reader_writer.GetSeedData().data, IsEmpty());
EXPECT_THAT(seed_reader_writer.GetSeedData().signature, IsEmpty());
EXPECT_EQ(seed_reader_writer.GetSeedData().milestone, 0);
EXPECT_TRUE(seed_reader_writer.GetSeedData().seed_date.is_null());
// Session country code is not cleared.
EXPECT_THAT(seed_reader_writer.GetSeedData().session_country_code,
Not(IsEmpty()));
// Verify seed cleared correctly in Local State prefs and that seed file is
// deleted.
EXPECT_THAT(local_state_.GetString(GetParam().seed_fields_prefs.seed),
IsEmpty());
EXPECT_THAT(local_state_.GetString(GetParam().seed_fields_prefs.signature),
IsEmpty());
EXPECT_EQ(local_state_.GetInteger(GetParam().seed_fields_prefs.milestone), 0);
EXPECT_EQ(local_state_.GetTime(GetParam().seed_fields_prefs.seed_date),
base::Time());
EXPECT_EQ(
local_state_.GetTime(GetParam().seed_fields_prefs.client_fetch_time),
base::Time());
// Session country code is not cleared.
EXPECT_THAT(
local_state_.GetString(GetParam().seed_fields_prefs.session_country_code),
Not(IsEmpty()));
EXPECT_FALSE(base::PathExists(temp_seed_file_path_));
}
// Verifies that session country code is cleared from Local State for clients
// using local state to store seeds.
TEST_P(SeedReaderWriterLocalStateGroupsTest, ClearSessionCountryCode) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Create and store seed.
local_state_.SetString(GetParam().seed_fields_prefs.session_country_code,
"us");
// Clear seed and force file delete.
seed_reader_writer.ClearSessionCountry();
file_writer_thread_.FlushForTesting();
EXPECT_THAT(seed_reader_writer.GetSeedData().session_country_code, IsEmpty());
EXPECT_THAT(
local_state_.GetString(GetParam().seed_fields_prefs.session_country_code),
IsEmpty());
}
// Verifies clients using local state to store seeds read seeds from local
// state.
TEST_P(SeedReaderWriterLocalStateGroupsTest, ReadLocalStateBasedSeed) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Create and store seed.
ASSERT_TRUE(base::WriteFile(temp_seed_file_path_, "unused seed"));
const std::string_view seed_data_field = GetParam().seed_fields_prefs.seed;
local_state_.SetString(seed_data_field,
base::Base64Encode(CreateCompressedVariationsSeed()));
// Initialize seed_reader_writer with test thread.
base::HistogramTester histogram_tester;
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
// Ensure seed data loaded from prefs, not seed file.
ASSERT_EQ(StoredSeed::StorageFormat::kCompressedAndBase64Encoded,
seed_reader_writer.GetSeedData().storage_format);
ASSERT_EQ(local_state_.GetString(GetParam().seed_fields_prefs.seed),
seed_reader_writer.GetSeedData().data);
histogram_tester.ExpectTotalCount(
base::StrCat(
{"Variations.SeedFileRead.",
base::Contains(seed_data_field, "Safe") ? "Safe" : "Latest"}),
/*expected_count=*/0);
}
// Verifies that writing seeds with an empty path for `seed_file_dir` does not
// cause a crash.
TEST_P(SeedReaderWriterLocalStateGroupsTest, EmptySeedFilePathIsValid) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer and an empty file
// path.
SeedReaderWriter seed_reader_writer(
&local_state_,
/*seed_file_dir=*/base::FilePath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string seed_data = CreateVariationsSeed();
const base::Time seed_date = base::Time::Now();
const base::Time fetch_time = base::Time::Now();
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = seed_data,
.signature = "signature",
.milestone = 2,
.seed_date = seed_date,
.client_fetch_time = fetch_time,
});
// Ensure there's no pending write.
EXPECT_FALSE(timer_.IsRunning());
// Verify seed stored correctly, should only be found in Local State prefs.
const std::string base64_compressed_seed =
base::Base64Encode(Gzip(seed_data));
EXPECT_EQ(local_state_.GetString(GetParam().seed_fields_prefs.seed),
base64_compressed_seed);
EXPECT_EQ(local_state_.GetString(GetParam().seed_fields_prefs.signature),
"signature");
EXPECT_EQ(local_state_.GetInteger(GetParam().seed_fields_prefs.milestone), 2);
EXPECT_EQ(local_state_.GetTime(GetParam().seed_fields_prefs.seed_date),
seed_date);
EXPECT_EQ(local_state_.GetTime(GetParam().seed_fields_prefs.client_fetch_time),
fetch_time);
}
TEST_P(SeedReaderWriterLocalStateGroupsTest, ReadSeedData) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer and an empty file
// path.
SeedReaderWriter seed_reader_writer(
&local_state_,
/*seed_file_dir=*/base::FilePath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
const std::string seed_data = CreateVariationsSeed();
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = seed_data,
.signature = "seed signature",
});
std::string read_seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&read_seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kSuccess);
EXPECT_EQ(read_seed_data, seed_data);
EXPECT_EQ(base64_seed_signature, "seed signature");
}
TEST_P(SeedReaderWriterLocalStateGroupsTest, ReadSeedDataCorruptBase64) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer and an empty file
// path.
SeedReaderWriter seed_reader_writer(
&local_state_,
/*seed_file_dir=*/base::FilePath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
seed_reader_writer.StoreRawSeedForTesting("invalid base64");
std::string seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kCorruptBase64);
}
TEST_P(SeedReaderWriterLocalStateGroupsTest, ReadSeedDataCorruptGzip) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer and an empty file
// path.
SeedReaderWriter seed_reader_writer(
&local_state_,
/*seed_file_dir=*/base::FilePath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
std::string compressed_seed = CreateCompressedVariationsSeed();
compressed_seed[5] ^= 0xFF;
compressed_seed[10] ^= 0xFF;
const std::string base64_compressed_seed =
base::Base64Encode(compressed_seed);
seed_reader_writer.StoreRawSeedForTesting(base64_compressed_seed);
std::string seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kCorruptGzip);
}
TEST_P(SeedReaderWriterLocalStateGroupsTest, ReadSeedDataExceedsSizeLimit) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer and an empty file
// path.
SeedReaderWriter seed_reader_writer(
&local_state_,
/*seed_file_dir=*/base::FilePath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
const std::string base64_compressed_seed =
base::Base64Encode(Gzip(std::string(51 * 1024 * 1024, 'A')));
seed_reader_writer.StoreRawSeedForTesting(base64_compressed_seed);
std::string seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kExceedsUncompressedSizeLimit);
}
INSTANTIATE_TEST_SUITE_P(
NoGroup,
SeedReaderWriterLocalStateGroupsTest,
::testing::ConvertGenerator<SeedReaderWriterTestParams::TupleT>(
::testing::Combine(::testing::Values(kRegularSeedFieldsPrefs,
kSafeSeedFieldsPrefs),
::testing::Values(kNoGroup),
::testing::Values(version_info::Channel::UNKNOWN))));
INSTANTIATE_TEST_SUITE_P(
ControlAndDefaultGroup,
SeedReaderWriterLocalStateGroupsTest,
::testing::ConvertGenerator<SeedReaderWriterTestParams::TupleT>(
::testing::Combine(::testing::Values(kRegularSeedFieldsPrefs,
kSafeSeedFieldsPrefs),
::testing::Values(kControlGroup, kDefaultGroup),
::testing::Values(version_info::Channel::UNKNOWN,
version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA,
version_info::Channel::STABLE))));
class SeedReaderWriterAllGroupsTest : public SeedReaderWriterGroupTest {};
INSTANTIATE_TEST_SUITE_P(
AllGroups,
SeedReaderWriterAllGroupsTest,
::testing::ConvertGenerator<SeedReaderWriterTestParams::TupleT>(
::testing::Combine(
::testing::Values(kRegularSeedFieldsPrefs, kSafeSeedFieldsPrefs),
::testing::Values(kControlGroup, kDefaultGroup, kSeedFilesGroup),
::testing::Values(version_info::Channel::UNKNOWN,
version_info::Channel::CANARY,
version_info::Channel::DEV,
version_info::Channel::BETA,
version_info::Channel::STABLE))));
TEST_P(SeedReaderWriterAllGroupsTest, ReadSeedDataEmptySeedData) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = "",
.signature = "ignored signature",
});
ASSERT_THAT(seed_reader_writer.GetSeedData().data, IsEmpty());
std::string seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kEmpty);
}
TEST_P(SeedReaderWriterAllGroupsTest, ReadSeedDataSentinel) {
ASSERT_EQ(base::FieldTrialList::FindFullName(kSeedFileTrial),
GetParam().field_trial_group);
// Initialize seed_reader_writer with test thread and timer.
SeedReaderWriter seed_reader_writer(
&local_state_, /*seed_file_dir=*/temp_dir_.GetPath(), kSeedFilename,
GetParam().seed_fields_prefs, GetParam().channel,
entropy_providers_.get(), file_writer_thread_.task_runner());
seed_reader_writer.SetTimerForTesting(&timer_);
// Create and store seed.
seed_reader_writer.StoreValidatedSeedInfo(ValidatedSeedInfo{
.seed_data = kIdenticalToSafeSeedSentinel,
.signature = "ignored signature",
});
std::string seed_data;
std::string base64_seed_signature;
LoadSeedResult result =
seed_reader_writer.ReadSeedData(&seed_data, &base64_seed_signature);
EXPECT_EQ(result, LoadSeedResult::kSuccess);
EXPECT_EQ(seed_data, kIdenticalToSafeSeedSentinel);
}
} // namespace
} // namespace variations