blob: d763d16d9329fc4164dd12c4091b8745f5296295 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/preferences/tracked/pref_hash_filter.h"
#include <stddef.h>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_samples.h"
#include "base/metrics/statistics_recorder.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "components/os_crypt/async/browser/test_utils.h"
#include "components/os_crypt/async/common/encryptor.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "components/prefs/testing_pref_store.h"
#include "components/safe_browsing/core/common/features.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "services/preferences/public/cpp/tracked/configuration.h"
#include "services/preferences/public/cpp/tracked/mock_validation_delegate.h"
#include "services/preferences/public/cpp/tracked/pref_names.h"
#include "services/preferences/tracked/features.h"
#include "services/preferences/tracked/hash_store_contents.h"
#include "services/preferences/tracked/pref_hash_store.h"
#include "services/preferences/tracked/pref_hash_store_transaction.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
using EnforcementLevel =
prefs::mojom::TrackedPreferenceMetadata::EnforcementLevel;
using PrefTrackingStrategy =
prefs::mojom::TrackedPreferenceMetadata::PrefTrackingStrategy;
using ValueType = prefs::mojom::TrackedPreferenceMetadata::ValueType;
using ValueState =
prefs::mojom::TrackedPreferenceValidationDelegate::ValueState;
const char kAtomicPref[] = "atomic_pref";
const char kAtomicPref2[] = "atomic_pref2";
const char kAtomicPref3[] = "pref3";
const char kAtomicPref4[] = "pref4";
const char kDeprecatedTrackedDictionaryEntry[] = "dictionary.pref";
const char kDeprecatedUntrackedDictionary[] = "dictionary";
const char kReportOnlyPref[] = "report_only";
const char kReportOnlySplitPref[] = "report_only_split_pref";
const char kSplitPref[] = "split_pref";
const char kScheduleToFlushToDisk[] = "schedule_to_flush_to_disk";
const prefs::TrackedPreferenceMetadata kTestTrackedPrefs[] = {
{0, kAtomicPref, EnforcementLevel::ENFORCE_ON_LOAD,
PrefTrackingStrategy::ATOMIC, ValueType::PERSONAL},
{1, kReportOnlyPref, EnforcementLevel::NO_ENFORCEMENT,
PrefTrackingStrategy::ATOMIC, ValueType::IMPERSONAL},
{2, kSplitPref, EnforcementLevel::ENFORCE_ON_LOAD,
PrefTrackingStrategy::SPLIT, ValueType::IMPERSONAL},
{3, kReportOnlySplitPref, EnforcementLevel::NO_ENFORCEMENT,
PrefTrackingStrategy::SPLIT, ValueType::IMPERSONAL},
{4, kAtomicPref2, EnforcementLevel::ENFORCE_ON_LOAD,
PrefTrackingStrategy::ATOMIC, ValueType::IMPERSONAL},
{5, kAtomicPref3, EnforcementLevel::ENFORCE_ON_LOAD,
PrefTrackingStrategy::ATOMIC, ValueType::IMPERSONAL},
{6, kAtomicPref4, EnforcementLevel::ENFORCE_ON_LOAD,
PrefTrackingStrategy::ATOMIC, ValueType::IMPERSONAL},
};
} // namespace
// A PrefHashStore that allows simulation of CheckValue results and captures
// checked and stored values.
class MockPrefHashStore : public PrefHashStore {
public:
typedef std::pair<const void*, PrefTrackingStrategy> ValuePtrStrategyPair;
MockPrefHashStore()
: stamp_super_mac_result_(false),
is_super_mac_valid_result_(false),
store_encrypted_hash_called_(false),
transactions_performed_(0),
transaction_active_(false) {}
MockPrefHashStore(const MockPrefHashStore&) = delete;
MockPrefHashStore& operator=(const MockPrefHashStore&) = delete;
~MockPrefHashStore() override { EXPECT_FALSE(transaction_active_); }
// Set the result that will be returned when |path| is passed to
// |CheckValue/CheckSplitValue|.
void SetCheckResult(const std::string& path, ValueState result);
// Set the invalid_keys that will be returned when |path| is passed to
// |CheckSplitValue|. SetCheckResult should already have been called for
// |path| with |result == CHANGED| for this to make any sense.
void SetInvalidKeysResult(
const std::string& path,
const std::vector<std::string>& invalid_keys_result);
// Sets the value that will be returned from
// PrefHashStoreTransaction::StampSuperMAC().
void set_stamp_super_mac_result(bool result) {
stamp_super_mac_result_ = result;
}
bool StoreEncryptedHashCalled() const { return store_encrypted_hash_called_; }
void ClearTestState() { checked_values_.clear(); }
// Sets the value that will be returned from
// PrefHashStoreTransaction::IsSuperMACValid().
void set_is_super_mac_valid_result(bool result) {
is_super_mac_valid_result_ = result;
}
// Returns the number of transactions that were performed.
size_t transactions_performed() { return transactions_performed_; }
// Returns the number of paths checked.
size_t checked_paths_count() const { return checked_values_.size(); }
// Returns the number of paths stored.
size_t stored_paths_count() const { return owned_stored_values_.size(); }
// Returns the pointer value and strategy that was passed to
// |CheckHash/CheckSplitHash| for |path|. The returned pointer could since
// have been freed and is thus not safe to dereference.
ValuePtrStrategyPair checked_value(const std::string& path) const {
std::map<std::string, ValuePtrStrategyPair>::const_iterator value =
checked_values_.find(path);
if (value != checked_values_.end())
return value->second;
return std::make_pair(reinterpret_cast<void*>(0xBAD),
static_cast<PrefTrackingStrategy>(-1));
}
// Returns the pointer value that was passed to |StoreHash/StoreSplitHash| for
// |path|. The returned pointer could since have been freed and is thus not
// safe to dereference.
ValuePtrStrategyPair stored_value(const std::string& path) const {
auto it = owned_stored_values_.find(path);
if (it != owned_stored_values_.end()) {
return std::make_pair(it->second.first.get(), it->second.second);
}
return std::make_pair(reinterpret_cast<void*>(0xBAD),
static_cast<PrefTrackingStrategy>(-1));
}
// PrefHashStore implementation.
std::unique_ptr<PrefHashStoreTransaction> BeginTransaction(
HashStoreContents* storage,
const os_crypt_async::Encryptor* encryptor_ptr) override;
std::string ComputeMac(const std::string& path,
const base::Value* new_value) override;
base::Value::Dict ComputeSplitMacs(
const std::string& path,
const base::Value::Dict* split_values) override;
std::string ComputeEncryptedHash(
const std::string& path,
const base::Value* value,
const os_crypt_async::Encryptor* encryptor_ptr) override;
std::string ComputeEncryptedHash(
const std::string& path,
const base::Value::Dict* dict,
const os_crypt_async::Encryptor* encryptor_ptr) override;
base::Value::Dict ComputeSplitEncryptedHashes(
const std::string& path,
const base::Value::Dict* split_values,
const os_crypt_async::Encryptor* encryptor_ptr) override;
void SetTransactionCompletionCallback(base::OnceClosure callback);
private:
// A MockPrefHashStoreTransaction is handed to the caller on
// MockPrefHashStore::BeginTransaction(). It then stores state in its
// underlying MockPrefHashStore about calls it receives from that same caller
// which can later be verified in tests.
class MockPrefHashStoreTransaction : public PrefHashStoreTransaction {
public:
explicit MockPrefHashStoreTransaction(MockPrefHashStore* outer)
: outer_(outer) {}
MockPrefHashStoreTransaction(const MockPrefHashStoreTransaction&) = delete;
MockPrefHashStoreTransaction& operator=(
const MockPrefHashStoreTransaction&) = delete;
~MockPrefHashStoreTransaction() override {
outer_->transaction_active_ = false;
++outer_->transactions_performed_;
if (outer_->completion_callback_) {
// Post a task to avoid potential re-entrancy issues.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(outer_->completion_callback_));
}
}
// PrefHashStoreTransaction implementation.
std::string_view GetStoreUMASuffix() const override;
ValueState CheckValue(const std::string& path,
const base::Value* value) const override;
void StoreHash(const std::string& path,
const base::Value* new_value) override;
ValueState CheckSplitValue(
const std::string& path,
const base::Value::Dict* initial_split_value,
std::vector<std::string>* invalid_keys) const override;
void StoreSplitHash(const std::string& path,
const base::Value::Dict* split_value) override;
bool HasHash(const std::string& path) const override;
void ImportHash(const std::string& path, const base::Value* hash) override;
void ClearHash(const std::string& path) override;
void ClearEncryptedHash(const std::string& path) override;
bool IsSuperMACValid() const override;
bool StampSuperMac() override;
void StoreEncryptedHash(const std::string& path,
const base::Value* value) override {
outer_->store_encrypted_hash_called_ = true;
// Record this like a normal store operation in this simple mock.
// Pass the base value pointer directly.
outer_->RecordStoreHash(path, value, PrefTrackingStrategy::ATOMIC);
// Note: This doesn't store under the derived key, which might affect
// tests checking internal state directly, but suffices for making the
// class non-abstract and allowing PrefHashFilter calls.
}
void StoreSplitEncryptedHash(const std::string& path,
const base::Value::Dict* value) override {
outer_->store_encrypted_hash_called_ = true;
// Record this like a normal store operation in this simple mock.
// Pass the base value pointer directly.
outer_->RecordStoreHash(path, value, PrefTrackingStrategy::SPLIT);
}
std::optional<std::string> GetEncryptedHash(
const std::string& path) const override {
return std::nullopt;
}
std::optional<std::string> GetMac(const std::string& path) const override {
return std::nullopt;
}
bool HasEncryptedHash(const std::string& path) const override {
return false;
}
private:
raw_ptr<MockPrefHashStore> outer_;
};
// Records a call to this mock's CheckValue/CheckSplitValue methods.
ValueState RecordCheckValue(const std::string& path,
const void* value,
PrefTrackingStrategy strategy);
// Records a call to this mock's StoreHash/StoreSplitHash methods.
void RecordStoreHash(const std::string& path,
const void* new_value,
PrefTrackingStrategy strategy);
void ClearStoreHash(const std::string& path);
std::map<std::string, ValueState> check_results_;
std::map<std::string, std::vector<std::string>> invalid_keys_results_;
bool stamp_super_mac_result_;
bool is_super_mac_valid_result_;
bool store_encrypted_hash_called_;
base::OnceClosure completion_callback_;
std::map<std::string, ValuePtrStrategyPair> checked_values_;
std::map<std::string, ValuePtrStrategyPair> stored_values_;
std::map<std::string,
std::pair<std::unique_ptr<base::Value>, PrefTrackingStrategy>>
owned_stored_values_;
// Number of transactions that were performed via this MockPrefHashStore.
size_t transactions_performed_;
// Whether a transaction is currently active (only one transaction should be
// active at a time).
bool transaction_active_;
};
void MockPrefHashStore::SetCheckResult(const std::string& path,
ValueState result) {
check_results_.insert(std::make_pair(path, result));
}
void MockPrefHashStore::SetInvalidKeysResult(
const std::string& path,
const std::vector<std::string>& invalid_keys_result) {
// Ensure |check_results_| has a CHANGED entry for |path|.
std::map<std::string, ValueState>::const_iterator result =
check_results_.find(path);
ASSERT_TRUE(result != check_results_.end());
ASSERT_EQ(ValueState::CHANGED, result->second);
invalid_keys_results_.insert(std::make_pair(path, invalid_keys_result));
}
std::unique_ptr<PrefHashStoreTransaction> MockPrefHashStore::BeginTransaction(
HashStoreContents* storage,
const os_crypt_async::Encryptor* encryptor_ptr) {
EXPECT_FALSE(transaction_active_);
transaction_active_ = true;
// Pass this mock store instance to the nested transaction mock
return std::unique_ptr<PrefHashStoreTransaction>(
new MockPrefHashStoreTransaction(this));
}
std::string MockPrefHashStore::ComputeMac(const std::string& path,
const base::Value* new_value) {
return "atomic mac for: " + path;
}
base::Value::Dict MockPrefHashStore::ComputeSplitMacs(
const std::string& path,
const base::Value::Dict* split_values) {
base::Value::Dict macs_dict;
if (!split_values)
return macs_dict;
for (const auto item : *split_values) {
macs_dict.Set(item.first,
base::Value("split mac for: " + path + "/" + item.first));
}
return macs_dict;
}
std::string MockPrefHashStore::ComputeEncryptedHash(
const std::string& path,
const base::Value* value,
const os_crypt_async::Encryptor* encryptor_ptr) {
// Return a dummy value, actual calculation not needed for filter test
return "encrypted atomic hash for: " + path;
}
std::string MockPrefHashStore::ComputeEncryptedHash(
const std::string& path,
const base::Value::Dict* dict,
const os_crypt_async::Encryptor* encryptor_ptr) {
return "encrypted atomic hash for dict: " + path;
}
base::Value::Dict MockPrefHashStore::ComputeSplitEncryptedHashes(
const std::string& path,
const base::Value::Dict* split_values,
const os_crypt_async::Encryptor* encryptor_ptr) {
base::Value::Dict hashes_dict;
if (!split_values) {
return hashes_dict;
}
for (const auto item : *split_values) {
hashes_dict.Set(
item.first,
base::Value("encrypted split hash for: " + path + "/" + item.first));
}
return hashes_dict;
}
ValueState MockPrefHashStore::RecordCheckValue(const std::string& path,
const void* value,
PrefTrackingStrategy strategy) {
// Record that |path| was checked and validate that it wasn't previously
// checked.
EXPECT_TRUE(checked_values_
.insert(std::make_pair(path, std::make_pair(value, strategy)))
.second);
std::map<std::string, ValueState>::const_iterator result =
check_results_.find(path);
if (result != check_results_.end())
return result->second;
return ValueState::UNCHANGED;
}
void MockPrefHashStore::RecordStoreHash(const std::string& path,
const void* new_value_ptr,
PrefTrackingStrategy strategy) {
const base::Value* value_to_store =
static_cast<const base::Value*>(new_value_ptr);
std::unique_ptr<base::Value> owned_copy;
if (value_to_store) {
owned_copy = std::make_unique<base::Value>(
value_to_store->Clone()); // Store a CLONE
}
// Store the unique_ptr (which might be null if new_value_ptr was null)
// and the strategy. For simplicity, assuming overwrite is fine or paths are
// unique per transaction.
owned_stored_values_[path] = std::make_pair(std::move(owned_copy), strategy);
}
void MockPrefHashStore::ClearStoreHash(const std::string& path) {
owned_stored_values_.erase(path);
}
std::string_view
MockPrefHashStore::MockPrefHashStoreTransaction ::GetStoreUMASuffix() const {
return "unused";
}
ValueState MockPrefHashStore::MockPrefHashStoreTransaction::CheckValue(
const std::string& path,
const base::Value* value) const {
return outer_->RecordCheckValue(path, value, PrefTrackingStrategy::ATOMIC);
}
void MockPrefHashStore::MockPrefHashStoreTransaction::StoreHash(
const std::string& path,
const base::Value* new_value) {
outer_->RecordStoreHash(path, new_value, PrefTrackingStrategy::ATOMIC);
}
ValueState MockPrefHashStore::MockPrefHashStoreTransaction::CheckSplitValue(
const std::string& path,
const base::Value::Dict* initial_split_value,
std::vector<std::string>* invalid_keys) const {
EXPECT_TRUE(invalid_keys && invalid_keys->empty());
std::map<std::string, std::vector<std::string>>::const_iterator
invalid_keys_result = outer_->invalid_keys_results_.find(path);
if (invalid_keys_result != outer_->invalid_keys_results_.end()) {
invalid_keys->insert(invalid_keys->begin(),
invalid_keys_result->second.begin(),
invalid_keys_result->second.end());
}
return outer_->RecordCheckValue(path, initial_split_value,
PrefTrackingStrategy::SPLIT);
}
void MockPrefHashStore::MockPrefHashStoreTransaction::StoreSplitHash(
const std::string& path,
const base::Value::Dict* new_value) {
outer_->RecordStoreHash(path, new_value, PrefTrackingStrategy::SPLIT);
}
bool MockPrefHashStore::MockPrefHashStoreTransaction::HasHash(
const std::string& path) const {
ADD_FAILURE() << "Unexpected call.";
return false;
}
void MockPrefHashStore::MockPrefHashStoreTransaction::ImportHash(
const std::string& path,
const base::Value* hash) {
ADD_FAILURE() << "Unexpected call.";
}
void MockPrefHashStore::MockPrefHashStoreTransaction::ClearHash(
const std::string& path) {
// Allow this to be called by PrefHashFilter's deprecated tracked prefs
// cleanup tasks.
outer_->ClearStoreHash(path);
}
void MockPrefHashStore::MockPrefHashStoreTransaction::ClearEncryptedHash(
const std::string& encrypted_path) {
// Allow this to be called by PrefHashFilter's deprecated tracked prefs
// cleanup tasks.
outer_->ClearStoreHash(encrypted_path);
}
bool MockPrefHashStore::MockPrefHashStoreTransaction::IsSuperMACValid() const {
return outer_->is_super_mac_valid_result_;
}
bool MockPrefHashStore::MockPrefHashStoreTransaction::StampSuperMac() {
return outer_->stamp_super_mac_result_;
}
void MockPrefHashStore::SetTransactionCompletionCallback(
base::OnceClosure callback) {
completion_callback_ = std::move(callback);
}
std::vector<prefs::mojom::TrackedPreferenceMetadataPtr> GetConfiguration(
EnforcementLevel max_enforcement_level) {
auto configuration = prefs::ConstructTrackedConfiguration(kTestTrackedPrefs);
for (const auto& metadata : configuration) {
if (metadata->enforcement_level > max_enforcement_level)
metadata->enforcement_level = max_enforcement_level;
}
return configuration;
}
class MockHashStoreContents : public HashStoreContents {
public:
MockHashStoreContents() {}
MockHashStoreContents(const MockHashStoreContents&) = delete;
MockHashStoreContents& operator=(const MockHashStoreContents&) = delete;
// Returns the number of hashes stored.
size_t stored_hashes_count() const { return dictionary_.size(); }
// Returns the number of paths cleared.
size_t cleared_paths_count() const { return removed_entries_.size(); }
// Returns the stored MAC for an Atomic preference.
std::string GetStoredMac(const std::string& path) const;
// Returns the stored MAC for a Split preference.
std::string GetStoredSplitMac(const std::string& path,
const std::string& split_path) const;
// HashStoreContents implementation.
bool IsCopyable() const override;
std::unique_ptr<HashStoreContents> MakeCopy() const override;
std::string_view GetUMASuffix() const override;
void Reset() override;
bool GetMac(const std::string& path, std::string* out_value) override;
bool GetSplitMacs(const std::string& path,
std::map<std::string, std::string>* split_macs) override;
void SetMac(const std::string& path, const std::string& value) override;
void SetSplitMac(const std::string& path,
const std::string& split_path,
const std::string& value) override;
void ImportEntry(const std::string& path,
const base::Value* in_value) override;
bool RemoveEntry(const std::string& path) override;
const base::Value::Dict* GetContents() const override;
std::string GetSuperMac() const override;
void SetSuperMac(const std::string& super_mac) override;
private:
explicit MockHashStoreContents(MockHashStoreContents* origin_mock);
// Records calls to this mock's SetMac/SetSplitMac methods.
void RecordSetMac(const std::string& path, const std::string& mac) {
dictionary_.Set(path, mac);
}
void RecordSetSplitMac(const std::string& path,
const std::string& split_path,
const std::string& mac) {
dictionary_.SetByDottedPath(base::StrCat({path, ".", split_path}), mac);
}
// Records a call to this mock's RemoveEntry method.
void RecordRemoveEntry(const std::string& path) {
// Don't expect the same pref to be cleared more than once.
EXPECT_EQ(removed_entries_.end(), removed_entries_.find(path));
removed_entries_.insert(path);
}
base::Value::Dict dictionary_;
std::set<std::string> removed_entries_;
// The code being tested copies its HashStoreContents for use in a callback
// which can be executed during shutdown. To be able to capture the behavior
// of the copy, we make it forward calls to the mock it was created from.
// Once set, |origin_mock_| must outlive this instance.
raw_ptr<MockHashStoreContents> origin_mock_;
};
std::string MockHashStoreContents::GetStoredMac(const std::string& path) const {
const base::Value* out_value = dictionary_.Find(path);
if (out_value) {
EXPECT_TRUE(out_value->is_string());
return out_value->GetString();
}
return std::string();
}
std::string MockHashStoreContents::GetStoredSplitMac(
const std::string& path,
const std::string& split_path) const {
const base::Value* out_value = dictionary_.Find(path);
if (out_value) {
EXPECT_TRUE(out_value->is_dict());
out_value = out_value->GetDict().Find(split_path);
if (out_value) {
EXPECT_TRUE(out_value->is_string());
return out_value->GetString();
}
}
return std::string();
}
MockHashStoreContents::MockHashStoreContents(MockHashStoreContents* origin_mock)
: origin_mock_(origin_mock) {}
bool MockHashStoreContents::IsCopyable() const {
return true;
}
std::unique_ptr<HashStoreContents> MockHashStoreContents::MakeCopy() const {
// Return a new MockHashStoreContents which forwards all requests to this
// mock instance.
return std::unique_ptr<HashStoreContents>(
new MockHashStoreContents(const_cast<MockHashStoreContents*>(this)));
}
std::string_view MockHashStoreContents::GetUMASuffix() const {
return "Unused";
}
void MockHashStoreContents::Reset() {
ADD_FAILURE() << "Unexpected call.";
}
bool MockHashStoreContents::GetMac(const std::string& path,
std::string* out_value) {
ADD_FAILURE() << "Unexpected call.";
return false;
}
bool MockHashStoreContents::GetSplitMacs(
const std::string& path,
std::map<std::string, std::string>* split_macs) {
ADD_FAILURE() << "Unexpected call.";
return false;
}
void MockHashStoreContents::SetMac(const std::string& path,
const std::string& value) {
if (origin_mock_)
origin_mock_->RecordSetMac(path, value);
else
RecordSetMac(path, value);
}
void MockHashStoreContents::SetSplitMac(const std::string& path,
const std::string& split_path,
const std::string& value) {
if (origin_mock_)
origin_mock_->RecordSetSplitMac(path, split_path, value);
else
RecordSetSplitMac(path, split_path, value);
}
void MockHashStoreContents::ImportEntry(const std::string& path,
const base::Value* in_value) {
ADD_FAILURE() << "Unexpected call.";
}
bool MockHashStoreContents::RemoveEntry(const std::string& path) {
if (origin_mock_)
origin_mock_->RecordRemoveEntry(path);
else
RecordRemoveEntry(path);
return true;
}
const base::Value::Dict* MockHashStoreContents::GetContents() const {
ADD_FAILURE() << "Unexpected call.";
return nullptr;
}
std::string MockHashStoreContents::GetSuperMac() const {
ADD_FAILURE() << "Unexpected call.";
return std::string();
}
void MockHashStoreContents::SetSuperMac(const std::string& super_mac) {
ADD_FAILURE() << "Unexpected call.";
}
class PrefHashFilterTest : public testing::TestWithParam<EnforcementLevel>,
public prefs::mojom::ResetOnLoadObserver {
public:
PrefHashFilterTest()
: mock_pref_hash_store_(nullptr),
mock_validation_delegate_record_(new MockValidationDelegateRecord),
mock_validation_delegate_(mock_validation_delegate_record_),
validation_delegate_receiver_(&mock_validation_delegate_),
reset_recorded_(false) {}
PrefHashFilterTest(const PrefHashFilterTest&) = delete;
PrefHashFilterTest& operator=(const PrefHashFilterTest&) = delete;
void SetUp() override {
// By default, initialize with a synchronous OSCrypt instance for existing
// tests.
InitializeSyncOSCrypt();
Reset();
}
// Resets to the default state (feature off).
void Reset() {
ResetImpl(false /* enable_encrypted_hashing_feature */,
test_os_crypt_async_.get());
}
// The main reset implementation, allowing tests to control the feature flag
// and the OSCrypt instance.
void ResetImpl(bool enable_encrypted_hashing_feature,
os_crypt_async::OSCryptAsync* os_crypt_instance_to_use) {
feature_list_.Reset(); // Clear previous ScopedFeatureList state
if (enable_encrypted_hashing_feature) {
feature_list_.InitAndEnableFeature(tracked::kEncryptedPrefHashing);
} else {
feature_list_.InitAndDisableFeature(tracked::kEncryptedPrefHashing);
}
InitializePrefHashFilter(GetConfiguration(GetParam()),
os_crypt_instance_to_use);
}
// Stores |prefs| back in |pref_store_contents| and ensure
// |expected_schedule_write| matches the reported |schedule_write|.
void GetPrefsBack(bool expected_schedule_write,
base::Value::Dict prefs,
bool schedule_write) {
pref_store_contents_ = std::move(prefs);
EXPECT_EQ(expected_schedule_write, schedule_write);
}
base::test::SingleThreadTaskEnvironment task_environment_;
std::unique_ptr<os_crypt_async::OSCryptAsync> test_os_crypt_async_;
protected:
// Initializes a TestOSCryptAsync instance that will provide its encryptor
// asynchronously.
void InitializeAsyncOSCrypt() {
test_os_crypt_async_ = os_crypt_async::GetTestOSCryptAsyncForTesting(
false /* is_sync_default */);
}
// Initializes a TestOSCryptAsync instance that provides its encryptor
// synchronously upon request.
void InitializeSyncOSCrypt() {
test_os_crypt_async_ = os_crypt_async::GetTestOSCryptAsyncForTesting(
true /* is_sync_default */);
}
// Initializes |pref_hash_filter_| with a PrefHashFilter that uses a
// MockPrefHashStore. The raw pointer to the MockPrefHashStore (owned by the
// PrefHashFilter) is stored in |mock_pref_hash_store_|.
void InitializePrefHashFilter(
std::vector<prefs::mojom::TrackedPreferenceMetadataPtr> configuration,
os_crypt_async::OSCryptAsync* os_crypt) {
std::unique_ptr<MockPrefHashStore> temp_mock_pref_hash_store(
new MockPrefHashStore);
std::unique_ptr<MockPrefHashStore>
temp_mock_external_validation_pref_hash_store(new MockPrefHashStore);
std::unique_ptr<MockHashStoreContents>
temp_mock_external_validation_hash_store_contents(
new MockHashStoreContents);
mock_pref_hash_store_ = temp_mock_pref_hash_store.get();
mock_external_validation_pref_hash_store_ =
temp_mock_external_validation_pref_hash_store.get();
mock_external_validation_hash_store_contents_ =
temp_mock_external_validation_hash_store_contents.get();
validation_delegate_receiver_.reset();
reset_on_load_observer_receivers_.Clear();
mojo::PendingRemote<prefs::mojom::ResetOnLoadObserver>
reset_on_load_observer;
reset_on_load_observer_receivers_.Add(
this, reset_on_load_observer.InitWithNewPipeAndPassReceiver());
mojo::Remote<prefs::mojom::TrackedPreferenceValidationDelegate>
validation_delegate_remote(
validation_delegate_receiver_.BindNewPipeAndPassRemote());
auto validation_delegate_remote_ref =
base::MakeRefCounted<base::RefCountedData<
mojo::Remote<prefs::mojom::TrackedPreferenceValidationDelegate>>>(
std::move(validation_delegate_remote));
pref_hash_filter_ = std::make_unique<PrefHashFilter>(
std::move(temp_mock_pref_hash_store),
PrefHashFilter::StoreContentsPair(
std::move(temp_mock_external_validation_pref_hash_store),
std::move(temp_mock_external_validation_hash_store_contents)),
std::move(configuration), std::move(reset_on_load_observer),
std::move(validation_delegate_remote_ref), std::size(kTestTrackedPrefs),
os_crypt);
}
// Verifies whether a reset was reported by the PrefHashFiler. Also verifies
// that kPreferenceResetTime was set (or not) accordingly.
void VerifyRecordedReset(bool reset_expected) {
base::RunLoop().RunUntilIdle();
EXPECT_EQ(reset_expected, reset_recorded_);
EXPECT_EQ(reset_expected, !!pref_store_contents_.FindByDottedPath(
user_prefs::kPreferenceResetTime));
}
// Calls FilterOnLoad() on |pref_hash_Filter_|. |pref_store_contents_| is
// handed off, but should be given back to us synchronously through
// GetPrefsBack() as there is no FilterOnLoadInterceptor installed on
// |pref_hash_filter_|.
void DoFilterOnLoad(bool expected_final_prefs_altered) {
pref_hash_filter_->FilterOnLoad(
base::BindOnce(&PrefHashFilterTest::GetPrefsBack,
base::Unretained(this), expected_final_prefs_altered),
std::move(pref_store_contents_));
task_environment_.RunUntilIdle();
}
raw_ptr<MockPrefHashStore, DanglingUntriaged> mock_pref_hash_store_;
raw_ptr<MockPrefHashStore, DanglingUntriaged>
mock_external_validation_pref_hash_store_;
raw_ptr<MockHashStoreContents, DanglingUntriaged>
mock_external_validation_hash_store_contents_;
base::Value::Dict pref_store_contents_;
scoped_refptr<MockValidationDelegateRecord> mock_validation_delegate_record_;
std::unique_ptr<PrefHashFilter> pref_hash_filter_;
private:
void OnResetOnLoad() override {
// As-is |reset_recorded_| is only designed to remember a single reset, make
// sure none was previously recorded.
EXPECT_FALSE(reset_recorded_);
reset_recorded_ = true;
}
base::test::ScopedFeatureList feature_list_;
MockValidationDelegate mock_validation_delegate_;
mojo::Receiver<prefs::mojom::TrackedPreferenceValidationDelegate>
validation_delegate_receiver_;
mojo::ReceiverSet<prefs::mojom::ResetOnLoadObserver>
reset_on_load_observer_receivers_;
bool reset_recorded_;
};
TEST_P(PrefHashFilterTest, EmptyAndUnchanged) {
DoFilterOnLoad(false);
// All paths checked.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->checked_paths_count());
// No paths stored, since they all return |UNCHANGED|.
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
// Since there was nothing in |pref_store_contents_| the checked value should
// have been nullptr for all tracked preferences.
for (const auto& pref : kTestTrackedPrefs) {
ASSERT_FALSE(mock_pref_hash_store_->checked_value(pref.name).first);
}
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
VerifyRecordedReset(false);
// Delegate saw all paths, and all unchanged.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->recorded_validations_count());
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNCHANGED));
}
TEST_P(PrefHashFilterTest, StampSuperMACAltersStore) {
mock_pref_hash_store_->set_stamp_super_mac_result(true);
DoFilterOnLoad(true);
// No paths stored, since they all return |UNCHANGED|. The StampSuperMAC
// result is the only reason the prefs were considered altered.
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
}
TEST_P(PrefHashFilterTest, FilterTrackedPrefUpdate) {
base::Value::Dict root_dict;
std::string expected_string_content = "string value";
root_dict.Set(kAtomicPref, expected_string_content);
// No path should be stored on FilterUpdate.
pref_hash_filter_->FilterUpdate(kAtomicPref);
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
// One path should be stored on FilterSerializeData.
base::RunLoop run_loop;
mock_pref_hash_store_->SetTransactionCompletionCallback(
run_loop.QuitClosure());
pref_hash_filter_->FilterSerializeData(root_dict);
run_loop.Run(); // Ensure any posted tasks are run
ASSERT_EQ(1u, mock_pref_hash_store_->stored_paths_count());
MockPrefHashStore::ValuePtrStrategyPair stored_value =
mock_pref_hash_store_->stored_value(kAtomicPref);
// Compare value content, not pointer
ASSERT_TRUE(stored_value.first);
const base::Value* actual_stored_value =
static_cast<const base::Value*>(stored_value.first);
ASSERT_TRUE(actual_stored_value->is_string());
EXPECT_EQ(expected_string_content, actual_stored_value->GetString());
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_value.second);
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
VerifyRecordedReset(false);
}
TEST_P(PrefHashFilterTest, FilterTrackedPrefClearing) {
base::Value::Dict root_dict;
// We don't actually add the pref's value to root_dict to simulate that
// it was just cleared in the PrefStore.
// No path should be stored on FilterUpdate.
pref_hash_filter_->FilterUpdate(kAtomicPref);
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
// One path should be stored on FilterSerializeData, with no value.
pref_hash_filter_->FilterSerializeData(root_dict);
ASSERT_EQ(1u, mock_pref_hash_store_->stored_paths_count());
MockPrefHashStore::ValuePtrStrategyPair stored_value =
mock_pref_hash_store_->stored_value(kAtomicPref);
ASSERT_FALSE(stored_value.first);
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_value.second);
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
VerifyRecordedReset(false);
}
TEST_P(PrefHashFilterTest, FilterSplitPrefUpdate) {
base::Value::Dict root_dict;
// Store expected content
base::Value::Dict expected_dict_content;
expected_dict_content.Set("a", "foo");
expected_dict_content.Set("b", 1234);
root_dict.Set(kSplitPref, expected_dict_content.Clone());
// No path should be stored on FilterUpdate.
pref_hash_filter_->FilterUpdate(kSplitPref);
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
// One path should be stored on FilterSerializeData.
base::RunLoop run_loop;
mock_pref_hash_store_->SetTransactionCompletionCallback(
run_loop.QuitClosure());
pref_hash_filter_->FilterSerializeData(root_dict);
run_loop.Run();
ASSERT_EQ(1u, mock_pref_hash_store_->stored_paths_count());
MockPrefHashStore::ValuePtrStrategyPair stored_value_info =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_TRUE(stored_value_info.first);
const base::Value* actual_stored_value =
static_cast<const base::Value*>(stored_value_info.first);
ASSERT_TRUE(actual_stored_value->is_dict());
// Compare dictionary contents directly.
EXPECT_EQ(expected_dict_content, actual_stored_value->GetDict());
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_value_info.second);
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
VerifyRecordedReset(false);
}
TEST_P(PrefHashFilterTest, FilterTrackedSplitPrefClearing) {
base::Value::Dict root_dict;
// We don't actually add the pref's value to root_dict to simulate that
// it was just cleared in the PrefStore.
// No path should be stored on FilterUpdate.
pref_hash_filter_->FilterUpdate(kSplitPref);
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
// One path should be stored on FilterSerializeData, with no value.
pref_hash_filter_->FilterSerializeData(root_dict);
ASSERT_EQ(1u, mock_pref_hash_store_->stored_paths_count());
MockPrefHashStore::ValuePtrStrategyPair stored_value =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_FALSE(stored_value.first);
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_value.second);
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
VerifyRecordedReset(false);
}
TEST_P(PrefHashFilterTest, FilterUntrackedPrefUpdate) {
base::Value::Dict root_dict;
root_dict.Set("untracked", "some value");
pref_hash_filter_->FilterUpdate("untracked");
// No paths should be stored on FilterUpdate.
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
// Nor on FilterSerializeData.
pref_hash_filter_->FilterSerializeData(root_dict);
ASSERT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
// No transaction should even be started on FilterSerializeData() if there are
// no updates to perform.
ASSERT_EQ(0u, mock_pref_hash_store_->transactions_performed());
}
TEST_P(PrefHashFilterTest, MultiplePrefsFilterSerializeData) {
base::Value::Dict root_dict;
int expected_atomic_val = 1;
int expected_atomic3_val = 5;
base::Value::Dict expected_split_dict_content;
expected_split_dict_content.Set("a", true);
root_dict.Set(kAtomicPref, expected_atomic_val);
root_dict.Set(kAtomicPref2, 2);
root_dict.Set(kAtomicPref3, 3);
root_dict.Set("untracked", 4);
root_dict.Set(kSplitPref, expected_split_dict_content.Clone());
// Only update kAtomicPref, kAtomicPref3, and kSplitPref.
pref_hash_filter_->FilterUpdate(kAtomicPref);
pref_hash_filter_->FilterUpdate(kAtomicPref3);
pref_hash_filter_->FilterUpdate(kSplitPref);
root_dict.Set(kAtomicPref3, expected_atomic3_val);
base::RunLoop run_loop;
mock_pref_hash_store_->SetTransactionCompletionCallback(
run_loop.QuitClosure());
pref_hash_filter_->FilterSerializeData(root_dict);
run_loop.Run();
ASSERT_EQ(3u, mock_pref_hash_store_->stored_paths_count());
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
// Verify kAtomicPref
MockPrefHashStore::ValuePtrStrategyPair stored_value_atomic1_info =
mock_pref_hash_store_->stored_value(kAtomicPref);
ASSERT_TRUE(stored_value_atomic1_info.first);
const base::Value* actual_atomic1 =
static_cast<const base::Value*>(stored_value_atomic1_info.first);
ASSERT_TRUE(actual_atomic1->is_int());
EXPECT_EQ(expected_atomic_val, actual_atomic1->GetInt());
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_value_atomic1_info.second);
// Verify kAtomicPref3
MockPrefHashStore::ValuePtrStrategyPair stored_value_atomic3_info =
mock_pref_hash_store_->stored_value(kAtomicPref3);
ASSERT_TRUE(stored_value_atomic3_info.first);
const base::Value* actual_atomic3 =
static_cast<const base::Value*>(stored_value_atomic3_info.first);
ASSERT_TRUE(actual_atomic3->is_int());
EXPECT_EQ(expected_atomic3_val, actual_atomic3->GetInt());
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_value_atomic3_info.second);
// Verify kSplitPref
MockPrefHashStore::ValuePtrStrategyPair stored_value_split_info =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_TRUE(stored_value_split_info.first);
const base::Value* actual_split =
static_cast<const base::Value*>(stored_value_split_info.first);
ASSERT_TRUE(actual_split->is_dict());
EXPECT_EQ(expected_split_dict_content, actual_split->GetDict());
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_value_split_info.second);
}
TEST_P(PrefHashFilterTest, UnknownNullValue) {
ASSERT_FALSE(pref_store_contents_.contains(kAtomicPref));
ASSERT_FALSE(pref_store_contents_.contains(kSplitPref));
// nullptr values are always trusted by the PrefHashStore.
mock_pref_hash_store_->SetCheckResult(kAtomicPref,
ValueState::TRUSTED_NULL_VALUE);
mock_pref_hash_store_->SetCheckResult(kSplitPref,
ValueState::TRUSTED_NULL_VALUE);
DoFilterOnLoad(false);
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->checked_paths_count());
ASSERT_EQ(2u, mock_pref_hash_store_->stored_paths_count());
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
MockPrefHashStore::ValuePtrStrategyPair stored_atomic_value =
mock_pref_hash_store_->stored_value(kAtomicPref);
ASSERT_FALSE(stored_atomic_value.first);
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_atomic_value.second);
MockPrefHashStore::ValuePtrStrategyPair stored_split_value =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_FALSE(stored_split_value.first);
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_split_value.second);
// Delegate saw all prefs, two of which had the expected value_state.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->recorded_validations_count());
ASSERT_EQ(2u, mock_validation_delegate_record_->CountValidationsOfState(
ValueState::TRUSTED_NULL_VALUE));
ASSERT_EQ(std::size(kTestTrackedPrefs) - 2u,
mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNCHANGED));
const MockValidationDelegateRecord::ValidationEvent* validated_split_pref =
mock_validation_delegate_record_->GetEventForPath(kSplitPref);
ASSERT_EQ(PrefTrackingStrategy::SPLIT, validated_split_pref->strategy);
ASSERT_FALSE(validated_split_pref->is_personal);
const MockValidationDelegateRecord::ValidationEvent* validated_atomic_pref =
mock_validation_delegate_record_->GetEventForPath(kAtomicPref);
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, validated_atomic_pref->strategy);
ASSERT_TRUE(validated_atomic_pref->is_personal);
}
TEST_P(PrefHashFilterTest, InitialValueUnknown) {
std::string expected_atomic_string_content = "string value";
base::Value::Dict expected_split_dict_content;
expected_split_dict_content.Set("a", "foo");
expected_split_dict_content.Set("b", 1234);
pref_store_contents_.Set(kAtomicPref, expected_atomic_string_content);
pref_store_contents_.Set(kSplitPref, expected_split_dict_content.Clone());
ASSERT_TRUE(pref_store_contents_.contains(kAtomicPref));
ASSERT_TRUE(pref_store_contents_.contains(kSplitPref));
mock_pref_hash_store_->SetCheckResult(kAtomicPref,
ValueState::UNTRUSTED_UNKNOWN_VALUE);
mock_pref_hash_store_->SetCheckResult(kSplitPref,
ValueState::UNTRUSTED_UNKNOWN_VALUE);
// If we are enforcing, expect this to report changes.
DoFilterOnLoad(GetParam() >= EnforcementLevel::ENFORCE_ON_LOAD);
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->checked_paths_count());
ASSERT_EQ(2u, mock_pref_hash_store_->stored_paths_count());
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->recorded_validations_count());
ASSERT_EQ(2u, mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNTRUSTED_UNKNOWN_VALUE));
ASSERT_EQ(std::size(kTestTrackedPrefs) - 2u,
mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNCHANGED));
MockPrefHashStore::ValuePtrStrategyPair stored_atomic_value_info =
mock_pref_hash_store_->stored_value(kAtomicPref);
MockPrefHashStore::ValuePtrStrategyPair stored_split_value_info =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_atomic_value_info.second);
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_split_value_info.second);
if (GetParam() == EnforcementLevel::ENFORCE_ON_LOAD) {
ASSERT_FALSE(pref_store_contents_.contains(kAtomicPref));
ASSERT_FALSE(stored_atomic_value_info.first);
ASSERT_FALSE(pref_store_contents_.contains(kSplitPref));
ASSERT_FALSE(stored_split_value_info.first);
VerifyRecordedReset(true);
} else { // NO_ENFORCEMENT
const base::Value* atomic_value_in_store =
pref_store_contents_.Find(kAtomicPref);
ASSERT_TRUE(atomic_value_in_store);
ASSERT_TRUE(atomic_value_in_store->is_string());
EXPECT_EQ(expected_atomic_string_content,
atomic_value_in_store->GetString());
ASSERT_TRUE(stored_atomic_value_info.first);
const base::Value* actual_stored_atomic =
static_cast<const base::Value*>(stored_atomic_value_info.first);
ASSERT_TRUE(actual_stored_atomic->is_string());
EXPECT_EQ(expected_atomic_string_content,
actual_stored_atomic->GetString());
const base::Value* split_value_in_store =
pref_store_contents_.Find(kSplitPref);
ASSERT_TRUE(split_value_in_store);
ASSERT_TRUE(split_value_in_store->is_dict());
EXPECT_EQ(expected_split_dict_content, split_value_in_store->GetDict());
ASSERT_TRUE(stored_split_value_info.first);
const base::Value* actual_stored_split =
static_cast<const base::Value*>(stored_split_value_info.first);
ASSERT_TRUE(actual_stored_split->is_dict());
EXPECT_EQ(expected_split_dict_content, actual_stored_split->GetDict());
VerifyRecordedReset(false);
}
}
TEST_P(PrefHashFilterTest, InitialValueTrustedUnknown) {
std::string expected_atomic_string_content = "test";
base::Value::Dict expected_split_dict_content;
expected_split_dict_content.Set("a", "foo");
expected_split_dict_content.Set("b", 1234);
pref_store_contents_.Set(kAtomicPref, expected_atomic_string_content);
pref_store_contents_.Set(kSplitPref, expected_split_dict_content.Clone());
ASSERT_TRUE(pref_store_contents_.contains(kAtomicPref));
ASSERT_TRUE(pref_store_contents_.contains(kSplitPref));
mock_pref_hash_store_->SetCheckResult(kAtomicPref,
ValueState::TRUSTED_UNKNOWN_VALUE);
mock_pref_hash_store_->SetCheckResult(kSplitPref,
ValueState::TRUSTED_UNKNOWN_VALUE);
DoFilterOnLoad(false);
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->checked_paths_count());
ASSERT_EQ(2u, mock_pref_hash_store_->stored_paths_count());
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
// Delegate saw all prefs, two of which had the expected value_state.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->recorded_validations_count());
ASSERT_EQ(2u, mock_validation_delegate_record_->CountValidationsOfState(
ValueState::TRUSTED_UNKNOWN_VALUE));
ASSERT_EQ(std::size(kTestTrackedPrefs) - 2u,
mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNCHANGED));
// Seeding is always allowed for trusted unknown values.
const base::Value* atomic_value_in_store =
pref_store_contents_.Find(kAtomicPref);
ASSERT_TRUE(atomic_value_in_store);
ASSERT_TRUE(atomic_value_in_store->is_string());
EXPECT_EQ(expected_atomic_string_content, atomic_value_in_store->GetString());
MockPrefHashStore::ValuePtrStrategyPair stored_atomic_value_info =
mock_pref_hash_store_->stored_value(kAtomicPref);
ASSERT_TRUE(stored_atomic_value_info.first);
const base::Value* actual_stored_atomic =
static_cast<const base::Value*>(stored_atomic_value_info.first);
ASSERT_TRUE(actual_stored_atomic->is_string());
EXPECT_EQ(expected_atomic_string_content, actual_stored_atomic->GetString());
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_atomic_value_info.second);
const base::Value* split_value_in_store =
pref_store_contents_.Find(kSplitPref);
ASSERT_TRUE(split_value_in_store);
ASSERT_TRUE(split_value_in_store->is_dict());
EXPECT_EQ(expected_split_dict_content, split_value_in_store->GetDict());
MockPrefHashStore::ValuePtrStrategyPair stored_split_value_info =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_TRUE(stored_split_value_info.first);
const base::Value* actual_stored_split =
static_cast<const base::Value*>(stored_split_value_info.first);
ASSERT_TRUE(actual_stored_split->is_dict());
EXPECT_EQ(expected_split_dict_content, actual_stored_split->GetDict());
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_split_value_info.second);
}
TEST_P(PrefHashFilterTest, InitialValueChanged) {
int expected_atomic_int_content = 1234;
base::Value::Dict initial_split_dict_content;
initial_split_dict_content.Set("a", "foo");
initial_split_dict_content.Set("b", 1234);
initial_split_dict_content.Set("c", 56);
initial_split_dict_content.Set("d", false);
base::Value::Dict expected_final_split_dict_content;
expected_final_split_dict_content.Set("b", 1234);
expected_final_split_dict_content.Set("d", false);
pref_store_contents_.Set(kAtomicPref, expected_atomic_int_content);
pref_store_contents_.Set(kSplitPref, initial_split_dict_content.Clone());
ASSERT_TRUE(pref_store_contents_.contains(kAtomicPref));
ASSERT_TRUE(pref_store_contents_.contains(kSplitPref));
mock_pref_hash_store_->SetCheckResult(kAtomicPref, ValueState::CHANGED);
mock_pref_hash_store_->SetCheckResult(kSplitPref, ValueState::CHANGED);
std::vector<std::string> mock_invalid_keys;
mock_invalid_keys.push_back("a");
mock_invalid_keys.push_back("c");
mock_pref_hash_store_->SetInvalidKeysResult(kSplitPref, mock_invalid_keys);
DoFilterOnLoad(GetParam() >= EnforcementLevel::ENFORCE_ON_LOAD);
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->checked_paths_count());
ASSERT_EQ(2u, mock_pref_hash_store_->stored_paths_count());
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
MockPrefHashStore::ValuePtrStrategyPair stored_atomic_value_info =
mock_pref_hash_store_->stored_value(kAtomicPref);
MockPrefHashStore::ValuePtrStrategyPair stored_split_value_info =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_atomic_value_info.second);
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_split_value_info.second);
if (GetParam() == EnforcementLevel::ENFORCE_ON_LOAD) {
// Ensure the atomic pref was cleared and the hash for nullptr was restored
// if the current enforcement level prevents changes.
ASSERT_FALSE(pref_store_contents_.contains(kAtomicPref));
ASSERT_FALSE(stored_atomic_value_info.first);
// The split pref on the other hand should only have been stripped of its
// invalid keys.
const base::Value* split_value_in_store =
pref_store_contents_.Find(kSplitPref);
ASSERT_TRUE(split_value_in_store);
ASSERT_TRUE(split_value_in_store->is_dict());
EXPECT_EQ(expected_final_split_dict_content,
split_value_in_store->GetDict());
ASSERT_TRUE(stored_split_value_info.first);
const base::Value* actual_stored_split =
static_cast<const base::Value*>(stored_split_value_info.first);
ASSERT_TRUE(actual_stored_split->is_dict());
EXPECT_EQ(expected_final_split_dict_content,
actual_stored_split->GetDict());
VerifyRecordedReset(true);
} else { // NO_ENFORCEMENT
// Otherwise the value should have remained intact and the hash should have
// been updated to match it.
const base::Value* atomic_value_in_store =
pref_store_contents_.Find(kAtomicPref);
ASSERT_TRUE(atomic_value_in_store);
ASSERT_TRUE(atomic_value_in_store->is_int());
EXPECT_EQ(expected_atomic_int_content, atomic_value_in_store->GetInt());
ASSERT_TRUE(stored_atomic_value_info.first);
const base::Value* actual_stored_atomic =
static_cast<const base::Value*>(stored_atomic_value_info.first);
ASSERT_TRUE(actual_stored_atomic->is_int());
EXPECT_EQ(expected_atomic_int_content, actual_stored_atomic->GetInt());
const base::Value* split_value_in_store =
pref_store_contents_.Find(kSplitPref);
ASSERT_TRUE(split_value_in_store);
ASSERT_TRUE(split_value_in_store->is_dict());
EXPECT_EQ(initial_split_dict_content, split_value_in_store->GetDict());
ASSERT_TRUE(stored_split_value_info.first);
const base::Value* actual_stored_split =
static_cast<const base::Value*>(stored_split_value_info.first);
ASSERT_TRUE(actual_stored_split->is_dict());
EXPECT_EQ(initial_split_dict_content, actual_stored_split->GetDict());
VerifyRecordedReset(false);
}
}
TEST_P(PrefHashFilterTest, EmptyCleared) {
ASSERT_FALSE(pref_store_contents_.contains(kAtomicPref));
ASSERT_FALSE(pref_store_contents_.contains(kSplitPref));
mock_pref_hash_store_->SetCheckResult(kAtomicPref, ValueState::CLEARED);
mock_pref_hash_store_->SetCheckResult(kSplitPref, ValueState::CLEARED);
DoFilterOnLoad(false);
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->checked_paths_count());
ASSERT_EQ(2u, mock_pref_hash_store_->stored_paths_count());
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
// Delegate saw all prefs, two of which had the expected value_state.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->recorded_validations_count());
ASSERT_EQ(2u, mock_validation_delegate_record_->CountValidationsOfState(
ValueState::CLEARED));
ASSERT_EQ(std::size(kTestTrackedPrefs) - 2u,
mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNCHANGED));
// Regardless of the enforcement level, the only thing that should be done is
// to restore the hash for nullptr. The value itself should still be nullptr.
ASSERT_FALSE(pref_store_contents_.contains(kAtomicPref));
MockPrefHashStore::ValuePtrStrategyPair stored_atomic_value =
mock_pref_hash_store_->stored_value(kAtomicPref);
ASSERT_FALSE(stored_atomic_value.first);
ASSERT_EQ(PrefTrackingStrategy::ATOMIC, stored_atomic_value.second);
ASSERT_FALSE(pref_store_contents_.contains(kSplitPref));
MockPrefHashStore::ValuePtrStrategyPair stored_split_value =
mock_pref_hash_store_->stored_value(kSplitPref);
ASSERT_FALSE(stored_split_value.first);
ASSERT_EQ(PrefTrackingStrategy::SPLIT, stored_split_value.second);
}
TEST_P(PrefHashFilterTest, DontResetReportOnly) {
int expected_atomic_val = 1;
int expected_atomic2_val = 2;
int expected_report_only_val = 3;
base::Value::Dict expected_report_only_split_val_content;
expected_report_only_split_val_content.Set("a", 1234);
pref_store_contents_.Set(kAtomicPref, expected_atomic_val);
pref_store_contents_.Set(kAtomicPref2, expected_atomic2_val);
pref_store_contents_.Set(kReportOnlyPref, expected_report_only_val);
pref_store_contents_.Set(kReportOnlySplitPref,
expected_report_only_split_val_content.Clone());
mock_pref_hash_store_->SetCheckResult(kAtomicPref, ValueState::CHANGED);
mock_pref_hash_store_->SetCheckResult(kAtomicPref2, ValueState::CHANGED);
mock_pref_hash_store_->SetCheckResult(kReportOnlyPref, ValueState::CHANGED);
mock_pref_hash_store_->SetCheckResult(kReportOnlySplitPref,
ValueState::CHANGED);
DoFilterOnLoad(GetParam() >= EnforcementLevel::ENFORCE_ON_LOAD);
// All prefs should be checked and a new hash should be stored for each tested
// pref.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->checked_paths_count());
ASSERT_EQ(4u, mock_pref_hash_store_->stored_paths_count());
ASSERT_EQ(1u, mock_pref_hash_store_->transactions_performed());
// Delegate saw all prefs, four of which had the expected value_state.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->recorded_validations_count());
ASSERT_EQ(4u, mock_validation_delegate_record_->CountValidationsOfState(
ValueState::CHANGED));
ASSERT_EQ(std::size(kTestTrackedPrefs) - 4u,
mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNCHANGED));
// No matter what the enforcement level is, the report only pref should never
// be reset.
ASSERT_TRUE(pref_store_contents_.contains(kReportOnlyPref));
ASSERT_TRUE(pref_store_contents_.contains(kReportOnlySplitPref));
// Compare content for report-only prefs
MockPrefHashStore::ValuePtrStrategyPair stored_report_only_info =
mock_pref_hash_store_->stored_value(kReportOnlyPref);
ASSERT_TRUE(stored_report_only_info.first);
const base::Value* actual_stored_report_only =
static_cast<const base::Value*>(stored_report_only_info.first);
ASSERT_TRUE(actual_stored_report_only->is_int());
EXPECT_EQ(expected_report_only_val, actual_stored_report_only->GetInt());
MockPrefHashStore::ValuePtrStrategyPair stored_report_only_split_info =
mock_pref_hash_store_->stored_value(kReportOnlySplitPref);
ASSERT_TRUE(stored_report_only_split_info.first);
const base::Value* actual_stored_report_only_split =
static_cast<const base::Value*>(stored_report_only_split_info.first);
ASSERT_TRUE(actual_stored_report_only_split->is_dict());
EXPECT_EQ(expected_report_only_split_val_content,
actual_stored_report_only_split->GetDict());
if (GetParam() == EnforcementLevel::ENFORCE_ON_LOAD) {
ASSERT_FALSE(pref_store_contents_.contains(kAtomicPref));
ASSERT_FALSE(pref_store_contents_.contains(kAtomicPref2));
ASSERT_FALSE(mock_pref_hash_store_->stored_value(kAtomicPref).first);
ASSERT_FALSE(mock_pref_hash_store_->stored_value(kAtomicPref2).first);
VerifyRecordedReset(true);
} else { // NO_ENFORCEMENT
// Compare content for enforced prefs if not reset
const base::Value* atomic_val_in_store =
pref_store_contents_.Find(kAtomicPref);
ASSERT_TRUE(atomic_val_in_store && atomic_val_in_store->is_int());
EXPECT_EQ(expected_atomic_val, atomic_val_in_store->GetInt());
MockPrefHashStore::ValuePtrStrategyPair stored_atomic_info =
mock_pref_hash_store_->stored_value(kAtomicPref);
ASSERT_TRUE(stored_atomic_info.first);
const base::Value* actual_stored_atomic =
static_cast<const base::Value*>(stored_atomic_info.first);
ASSERT_TRUE(actual_stored_atomic->is_int());
EXPECT_EQ(expected_atomic_val, actual_stored_atomic->GetInt());
const base::Value* atomic2_val_in_store =
pref_store_contents_.Find(kAtomicPref2);
ASSERT_TRUE(atomic2_val_in_store && atomic2_val_in_store->is_int());
EXPECT_EQ(expected_atomic2_val, atomic2_val_in_store->GetInt());
MockPrefHashStore::ValuePtrStrategyPair stored_atomic2_info =
mock_pref_hash_store_->stored_value(kAtomicPref2);
ASSERT_TRUE(stored_atomic2_info.first);
const base::Value* actual_stored_atomic2 =
static_cast<const base::Value*>(stored_atomic2_info.first);
ASSERT_TRUE(actual_stored_atomic2->is_int());
EXPECT_EQ(expected_atomic2_val, actual_stored_atomic2->GetInt());
VerifyRecordedReset(false);
}
}
TEST_P(PrefHashFilterTest, CallFilterSerializeDataCallbacks) {
base::Value::Dict root_dict;
base::Value::Dict dict_value;
dict_value.Set("a", true);
root_dict.Set(kAtomicPref, 1);
root_dict.Set(kAtomicPref2, 2);
root_dict.Set(kSplitPref, std::move(dict_value));
// Skip updating kAtomicPref2.
pref_hash_filter_->FilterUpdate(kAtomicPref);
pref_hash_filter_->FilterUpdate(kSplitPref);
PrefHashFilter::OnWriteCallbackPair callbacks =
pref_hash_filter_->FilterSerializeData(root_dict);
ASSERT_FALSE(callbacks.first.is_null());
// Prefs should be cleared from the external validation store only once the
// before-write callback is run.
ASSERT_EQ(
0u, mock_external_validation_hash_store_contents_->cleared_paths_count());
std::move(callbacks.first).Run();
ASSERT_EQ(
2u, mock_external_validation_hash_store_contents_->cleared_paths_count());
// No pref write should occur before the after-write callback is run.
ASSERT_EQ(
0u, mock_external_validation_hash_store_contents_->stored_hashes_count());
std::move(callbacks.second).Run(true);
ASSERT_EQ(
2u, mock_external_validation_hash_store_contents_->stored_hashes_count());
ASSERT_EQ(
"atomic mac for: atomic_pref",
mock_external_validation_hash_store_contents_->GetStoredMac(kAtomicPref));
ASSERT_EQ("split mac for: split_pref/a",
mock_external_validation_hash_store_contents_->GetStoredSplitMac(
kSplitPref, "a"));
// The callbacks should write directly to the contents without going through
// a pref hash store.
ASSERT_EQ(0u,
mock_external_validation_pref_hash_store_->stored_paths_count());
}
TEST_P(PrefHashFilterTest, CallFilterSerializeDataCallbacksWithFailure) {
base::Value::Dict root_dict;
root_dict.Set(kAtomicPref, 1);
// Only update kAtomicPref.
pref_hash_filter_->FilterUpdate(kAtomicPref);
PrefHashFilter::OnWriteCallbackPair callbacks =
pref_hash_filter_->FilterSerializeData(root_dict);
ASSERT_FALSE(callbacks.first.is_null());
std::move(callbacks.first).Run();
// The pref should have been cleared from the external validation store.
ASSERT_EQ(
1u, mock_external_validation_hash_store_contents_->cleared_paths_count());
std::move(callbacks.second).Run(false);
// Expect no writes to the external validation hash store contents.
ASSERT_EQ(0u,
mock_external_validation_pref_hash_store_->stored_paths_count());
ASSERT_EQ(
0u, mock_external_validation_hash_store_contents_->stored_hashes_count());
}
TEST_P(PrefHashFilterTest, ExternalValidationValueChanged) {
pref_store_contents_.Set(kAtomicPref, 1234);
base::Value::Dict dict_value;
dict_value.Set("a", "foo");
dict_value.Set("b", 1234);
dict_value.Set("c", 56);
dict_value.Set("d", false);
pref_store_contents_.Set(kSplitPref, std::move(dict_value));
mock_external_validation_pref_hash_store_->SetCheckResult(
kAtomicPref, ValueState::CHANGED);
mock_external_validation_pref_hash_store_->SetCheckResult(
kSplitPref, ValueState::CHANGED);
std::vector<std::string> mock_invalid_keys;
mock_invalid_keys.push_back("a");
mock_invalid_keys.push_back("c");
mock_external_validation_pref_hash_store_->SetInvalidKeysResult(
kSplitPref, mock_invalid_keys);
DoFilterOnLoad(false);
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_external_validation_pref_hash_store_->checked_paths_count());
ASSERT_EQ(2u,
mock_external_validation_pref_hash_store_->stored_paths_count());
ASSERT_EQ(
1u, mock_external_validation_pref_hash_store_->transactions_performed());
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->recorded_validations_count());
// Regular validation should not have any CHANGED prefs.
ASSERT_EQ(std::size(kTestTrackedPrefs),
mock_validation_delegate_record_->CountValidationsOfState(
ValueState::UNCHANGED));
// External validation should have two CHANGED prefs (kAtomic and kSplit).
ASSERT_EQ(2u,
mock_validation_delegate_record_->CountExternalValidationsOfState(
ValueState::CHANGED));
ASSERT_EQ(std::size(kTestTrackedPrefs) - 2u,
mock_validation_delegate_record_->CountExternalValidationsOfState(
ValueState::UNCHANGED));
}
TEST_P(PrefHashFilterTest, CleanupDeprecatedTrackedDictionary) {
// Fake a preference value and stored hash from an old version of Chrome.
base::Value pref_value(1234);
pref_store_contents_.SetByDottedPath(kDeprecatedTrackedDictionaryEntry, 1234);
{
std::unique_ptr<PrefHashStoreTransaction> transaction(
mock_pref_hash_store_->PrefHashStore::BeginTransaction(nullptr));
transaction->StoreHash(kDeprecatedTrackedDictionaryEntry, &pref_value);
}
ASSERT_EQ(1u, mock_pref_hash_store_->stored_paths_count());
std::vector<const char*> test_deprecated_prefs{
kDeprecatedTrackedDictionaryEntry,
kDeprecatedUntrackedDictionary,
};
PrefHashFilter::SetDeprecatedPrefsForTesting(test_deprecated_prefs);
DoFilterOnLoad(false);
EXPECT_EQ(0u, mock_pref_hash_store_->stored_paths_count());
EXPECT_FALSE(pref_store_contents_.contains("dictionary.pref"));
EXPECT_FALSE(pref_store_contents_.contains("dictionary"));
}
INSTANTIATE_TEST_SUITE_P(PrefHashFilterTestInstance,
PrefHashFilterTest,
testing::Values(EnforcementLevel::NO_ENFORCEMENT,
EnforcementLevel::ENFORCE_ON_LOAD));
// A test fixture for the PrefHashFilter that specifically handles testing the
// new encrypted hashing and deferred validation logic.
class PrefHashFilterEncryptedTest : public PrefHashFilterTest {
public:
// Helper function to handle the deferred revalidation callback.
void OnDeferredRevalidationComplete(bool* callback_ran_flag,
base::OnceClosure quit_closure) {
*callback_ran_flag = true;
std::move(quit_closure).Run();
}
protected:
void SetUp() override {
// This fixture manually controls its setup, so the base SetUp is not
// needed.
}
void TearDown() override {
// Ensure the dependency is always broken after each test.
if (pref_hash_filter_) {
pref_hash_filter_->SetPrefService(nullptr);
}
PrefHashFilterTest::TearDown();
}
// A mock PrefService that allows us to observe calls to ClearPref.
class MockPrefService : public TestingPrefServiceSimple {
public:
MockPrefService() = default;
~MockPrefService() override = default;
void ClearPref(const std::string& path) {
cleared_prefs_.insert(path);
TestingPrefServiceSimple::ClearPref(path);
}
bool WasCleared(const std::string& path) const {
return cleared_prefs_.count(path);
}
void ClearClearedPrefsForTesting() { cleared_prefs_.clear(); }
private:
std::set<std::string> cleared_prefs_;
};
std::unique_ptr<MockPrefService> mock_pref_service_;
};
TEST_P(PrefHashFilterEncryptedTest, FallbackPathInFilterSerializeData) {
InitializeSyncOSCrypt();
ResetImpl(true, test_os_crypt_async_.get());
pref_hash_filter_->FilterUpdate(kAtomicPref);
base::Value::Dict root_dict;
root_dict.Set(kAtomicPref, "value");
base::RunLoop run_loop;
mock_pref_hash_store_->SetTransactionCompletionCallback(
run_loop.QuitClosure());
pref_hash_filter_->FilterSerializeData(root_dict);
run_loop.Run();
EXPECT_TRUE(mock_pref_hash_store_->StoreEncryptedHashCalled());
EXPECT_EQ(std::size(kTestTrackedPrefs),
mock_pref_hash_store_->stored_paths_count());
}
TEST_P(PrefHashFilterEncryptedTest, PostsDeferredTaskOnlyWhenFeatureEnabled) {
InitializeAsyncOSCrypt();
mock_pref_service_ = std::make_unique<MockPrefService>();
// Also register the reset pref as a matter of good practice.
mock_pref_service_->registry()->RegisterStringPref(kScheduleToFlushToDisk,
"0");
bool callback_ran = false;
auto set_callback = [&](base::RunLoop& run_loop) {
pref_hash_filter_->SetOnDeferredRevalidationCompleteForTesting(
base::BindLambdaForTesting([&]() {
callback_ran = true;
run_loop.Quit();
}));
};
// 1. Test with feature DISABLED.
{
callback_ran = false;
base::RunLoop completion_run_loop;
ResetImpl(false, test_os_crypt_async_.get());
pref_hash_filter_->SetPrefService(mock_pref_service_.get());
set_callback(completion_run_loop);
DoFilterOnLoad(false);
completion_run_loop.Run();
EXPECT_TRUE(callback_ran);
}
// 2. Test with feature ENABLED.
{
callback_ran = false;
base::RunLoop revalidation_loop;
ResetImpl(true, test_os_crypt_async_.get());
pref_hash_filter_->SetPrefService(mock_pref_service_.get());
set_callback(revalidation_loop);
pref_hash_filter_->FilterOnLoad(
base::BindOnce(&PrefHashFilterTest::GetPrefsBack,
base::Unretained(this), false /* expected_altered */),
pref_store_contents_.Clone());
EXPECT_FALSE(callback_ran);
revalidation_loop.Run();
EXPECT_TRUE(callback_ran);
}
}
TEST_P(PrefHashFilterEncryptedTest, DeferredRevalidationSkipsIfValueChanged) {
InitializeAsyncOSCrypt();
ResetImpl(true, test_os_crypt_async_.get());
mock_pref_service_ = std::make_unique<MockPrefService>();
mock_pref_service_->registry()->RegisterStringPref(kAtomicPref, "");
mock_pref_service_->registry()->RegisterStringPref(kScheduleToFlushToDisk,
"0");
pref_hash_filter_->SetPrefService(mock_pref_service_.get());
pref_store_contents_.Set(kAtomicPref, "value_at_load");
mock_pref_hash_store_->SetCheckResult(kAtomicPref, ValueState::UNCHANGED);
pref_hash_filter_->FilterOnLoad(
base::BindOnce(&PrefHashFilterTest::GetPrefsBack, base::Unretained(this),
false),
std::move(pref_store_contents_));
mock_pref_service_->SetString(kAtomicPref, "value_changed_in_browser");
// Wait for the deferred revalidation to complete.
base::RunLoop revalidation_run_loop;
bool was_validation_performed = false;
pref_hash_filter_->SetOnDeferredRevalidationCompleteForTesting(base::BindOnce(
&PrefHashFilterEncryptedTest::OnDeferredRevalidationComplete,
base::Unretained(this), &was_validation_performed,
revalidation_run_loop.QuitClosure()));
mock_pref_hash_store_->ClearTestState();
revalidation_run_loop.Run();
EXPECT_FALSE(mock_pref_service_->WasCleared(kAtomicPref));
}
TEST_P(PrefHashFilterEncryptedTest, DeferredRevalidationSkipsIfValueCleared) {
InitializeAsyncOSCrypt();
ResetImpl(true, test_os_crypt_async_.get());
mock_pref_service_ = std::make_unique<MockPrefService>();
mock_pref_service_->registry()->RegisterStringPref(kAtomicPref, "");
mock_pref_service_->registry()->RegisterStringPref(kScheduleToFlushToDisk,
"0");
pref_hash_filter_->SetPrefService(mock_pref_service_.get());
pref_store_contents_.Set(kAtomicPref, "value_at_load");
mock_pref_hash_store_->SetCheckResult(kAtomicPref, ValueState::UNCHANGED);
// Set the encrypted hash explicitly to be invalid, so the deferred task will
// try to reset the pref.
mock_pref_hash_store_->SetCheckResult("prefix." + std::string(kAtomicPref),
ValueState::CHANGED);
pref_hash_filter_->FilterOnLoad(
base::BindOnce(&PrefHashFilterTest::GetPrefsBack, base::Unretained(this),
false),
std::move(pref_store_contents_));
mock_pref_service_->ClearPref(kAtomicPref);
base::RunLoop revalidation_run_loop;
bool was_validation_performed = false;
pref_hash_filter_->SetOnDeferredRevalidationCompleteForTesting(base::BindOnce(
&PrefHashFilterEncryptedTest::OnDeferredRevalidationComplete,
base::Unretained(this), &was_validation_performed,
revalidation_run_loop.QuitClosure()));
mock_pref_service_->ClearClearedPrefsForTesting();
revalidation_run_loop.Run();
EXPECT_TRUE(was_validation_performed);
// This means ClearPref should NOT be called a second time.
EXPECT_FALSE(mock_pref_service_->WasCleared(kAtomicPref));
}
INSTANTIATE_TEST_SUITE_P(PrefHashFilterTestInstance,
PrefHashFilterEncryptedTest,
testing::Values(EnforcementLevel::NO_ENFORCEMENT,
EnforcementLevel::ENFORCE_ON_LOAD));