blob: be7c3508197ed97521aab24f29df0e2f5dd5e2fd [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_cache.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "components/safe_browsing/core/common/proto/safebrowsingv5_alpha1.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/platform_test.h"
namespace safe_browsing {
class HashRealTimeCacheTest : public PlatformTest {
protected:
V5::Duration CreateCacheDuration(int seconds, int nanos) {
V5::Duration cache_duration;
cache_duration.set_seconds(seconds);
cache_duration.set_nanos(nanos);
return cache_duration;
}
// Does not populate the "attributes" field.
V5::FullHash CreateBasicFullHash(std::string full_hash_str,
std::vector<V5::ThreatType> threat_types) {
V5::FullHash full_hash_object;
full_hash_object.set_full_hash(full_hash_str);
for (const auto& threat_type : threat_types) {
auto* details = full_hash_object.add_full_hash_details();
details->set_threat_type(threat_type);
}
return full_hash_object;
}
void AddThreatTypeAndAttributes(V5::FullHash& full_hash_object,
V5::ThreatType threat_type,
std::vector<V5::ThreatAttribute> attributes) {
auto* details = full_hash_object.add_full_hash_details();
details->set_threat_type(threat_type);
for (const auto& attribute : attributes) {
details->add_attributes(attribute);
}
}
void CheckAndResetCacheHitsAndMisses(int num_hits, int num_misses) {
histogram_tester_->ExpectBucketCount("SafeBrowsing.HPRT.CacheHit",
/*sample=*/true,
/*expected_count=*/num_hits);
histogram_tester_->ExpectBucketCount("SafeBrowsing.HPRT.CacheHit",
/*sample=*/false,
/*expected_count=*/num_misses);
histogram_tester_ = std::make_unique<base::HistogramTester>();
}
void CheckAndResetCacheSizeOnClear(int num_hash_prefixes,
int num_full_hashes) {
histogram_tester_->ExpectBucketCount(
"SafeBrowsing.HPRT.Cache.HashPrefixCount",
/*sample=*/num_hash_prefixes,
/*expected_count=*/1);
histogram_tester_->ExpectBucketCount(
"SafeBrowsing.HPRT.Cache.FullHashCount",
/*sample=*/num_full_hashes,
/*expected_count=*/1);
histogram_tester_ = std::make_unique<base::HistogramTester>();
}
int GetNumCacheEntries(std::unique_ptr<HashRealTimeCache>& cache) {
// This includes expired entries that have not yet been cleaned up too.
return cache->cache_.size();
}
void CacheEntry(std::unique_ptr<HashRealTimeCache>& cache_internal,
std::string full_hash,
int cache_duration_seconds) {
cache_internal->CacheSearchHashesResponse(
{full_hash.substr(0, 4)},
{CreateBasicFullHash(full_hash, {V5::ThreatType::MALWARE})},
CreateCacheDuration(cache_duration_seconds, 0));
}
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<base::HistogramTester> histogram_tester_ =
std::make_unique<base::HistogramTester>();
};
TEST_F(HashRealTimeCacheTest, TestCacheMatching_EmptyCache) {
auto cache = std::make_unique<HashRealTimeCache>();
EXPECT_TRUE(cache->SearchCache({}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/0);
EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
EXPECT_TRUE(cache->SearchCache({"aaaa", "bbbb"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/2);
}
TEST_F(HashRealTimeCacheTest, TestCacheMatching_BasicFunctionality) {
base::HistogramTester histogram_tester;
auto cache = std::make_unique<HashRealTimeCache>();
// The below is done within a block to ensure that the cache works even once
// the inputs to CacheSearchHashesResponse have been destructed.
{
std::vector<std::string> requested_hash_prefixes = {"aaaa", "bbbb", "cccc",
"dddd"};
std::vector<V5::FullHash> response_full_hashes = {
CreateBasicFullHash(
"aaaa1111111111111111111111111111",
{V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
CreateBasicFullHash("aaaa2222222222222222222222222222",
{V5::ThreatType::MALWARE}),
CreateBasicFullHash("aaaa3333333333333333333333333333",
{V5::ThreatType::API_ABUSE}),
CreateBasicFullHash("cccc1111111111111111111111111111",
{V5::ThreatType::API_ABUSE,
V5::ThreatType::ABUSIVE_EXPERIENCE_VIOLATION,
V5::ThreatType::BETTER_ADS_VIOLATION,
V5::ThreatType::ABUSIVE_EXPERIENCE_VIOLATION,
V5::ThreatType::POTENTIALLY_HARMFUL_APPLICATION,
V5::ThreatType::SOCIAL_ENGINEERING_ADS}),
};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 0));
}
// Searching for no prefix or for prefixes not in the request should yield
// empty cache results.
EXPECT_TRUE(cache->SearchCache({}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/0);
EXPECT_TRUE(cache->SearchCache({"eeee"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
EXPECT_TRUE(cache->SearchCache({"eeee", "ffff"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/2);
std::set<std::string> hash_prefixes_to_search = {"aaaa", "bbbb", "cccc",
"dddd", "eeee", "ffff"};
auto cache_results = cache->SearchCache(hash_prefixes_to_search);
CheckAndResetCacheHitsAndMisses(/*num_hits=*/4, /*num_misses=*/2);
// Don't expect cache results for eeee and ffff, since they are not in the
// cache. Expect cache results for all other prefixes.
EXPECT_EQ(cache_results.size(), 4u);
EXPECT_TRUE(base::Contains(cache_results, "aaaa"));
EXPECT_TRUE(base::Contains(cache_results, "bbbb"));
EXPECT_TRUE(base::Contains(cache_results, "cccc"));
EXPECT_TRUE(base::Contains(cache_results, "dddd"));
EXPECT_FALSE(base::Contains(cache_results, "eeee"));
EXPECT_FALSE(base::Contains(cache_results, "ffff"));
// bbbb and dddd should both have empty results, because they did not have any
// corresponding full hashes.
EXPECT_TRUE(cache_results["bbbb"].empty());
EXPECT_TRUE(cache_results["dddd"].empty());
// cccc should also have empty results, because the threat types returned by
// the server for that full hash were not relevant for hash-prefix real-time
// lookups.
EXPECT_TRUE(cache_results["cccc"].empty());
// aaaa should match both aaaa...1 and aaaa...2, but not aaaa....3 due to
// irrelevant threat types.
EXPECT_EQ(cache_results["aaaa"].size(), 2u);
// aaaa...1 should only contain relevant threat types.
auto aaaa1_results = cache_results["aaaa"][0];
EXPECT_EQ(aaaa1_results.full_hash(), "aaaa1111111111111111111111111111");
auto aaaa1_details = aaaa1_results.full_hash_details();
EXPECT_EQ(aaaa1_details.size(), 3);
EXPECT_EQ(aaaa1_details[0].threat_type(), V5::ThreatType::SOCIAL_ENGINEERING);
EXPECT_TRUE(aaaa1_details[0].attributes().empty());
EXPECT_EQ(aaaa1_details[1].threat_type(), V5::ThreatType::MALWARE);
EXPECT_TRUE(aaaa1_details[1].attributes().empty());
EXPECT_EQ(aaaa1_details[2].threat_type(), V5::ThreatType::UNWANTED_SOFTWARE);
EXPECT_TRUE(aaaa1_details[2].attributes().empty());
// aaaa...2 should have one threat type (malware).
auto aaaa2_results = cache_results["aaaa"][1];
EXPECT_EQ(aaaa2_results.full_hash(), "aaaa2222222222222222222222222222");
auto aaaa2_details = aaaa2_results.full_hash_details();
EXPECT_EQ(aaaa2_details.size(), 1);
EXPECT_EQ(aaaa2_details[0].threat_type(), V5::ThreatType::MALWARE);
EXPECT_TRUE(aaaa2_details[0].attributes().empty());
}
TEST_F(HashRealTimeCacheTest, TestCacheMatching_Expiration) {
auto cache = std::make_unique<HashRealTimeCache>();
// The below are done within blocks to ensure that the cache works even once
// the inputs to CacheSearchHashesResponse have been destructed.
{
std::vector<std::string> requested_hash_prefixes = {"aaaa"};
std::vector<V5::FullHash> response_full_hashes = {
CreateBasicFullHash(
"aaaa1111111111111111111111111111",
{V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 0));
}
task_environment_.FastForwardBy(base::Seconds(299));
{
std::vector<std::string> requested_hash_prefixes = {"cccc"};
std::vector<V5::FullHash> response_full_hashes = {
CreateBasicFullHash("cccc1111111111111111111111111111",
{V5::ThreatType::MALWARE}),
};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 0));
}
// aaaa expires at 300 seconds. cccc expires at 599 seconds.
// Current time = 299 seconds. aaaa and cccc have not expired.
EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
EXPECT_FALSE(cache->SearchCache({"cccc"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
EXPECT_EQ(cache->SearchCache({"aaaa", "cccc"}).size(), 2u);
CheckAndResetCacheHitsAndMisses(/*num_hits=*/2, /*num_misses=*/0);
// Current time = 300 seconds. aaaa has expired. cccc has not expired.
task_environment_.FastForwardBy(base::Seconds(1));
EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
EXPECT_FALSE(cache->SearchCache({"cccc"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
EXPECT_EQ(cache->SearchCache({"aaaa", "cccc"}).size(), 1u);
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/1);
// Current time = 598 seconds. aaaa has expired. cccc has not expired.
task_environment_.FastForwardBy(base::Seconds(298));
EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
EXPECT_FALSE(cache->SearchCache({"cccc"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
EXPECT_EQ(cache->SearchCache({"aaaa", "cccc"}).size(), 1u);
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/1);
// Current time = 599 seconds. aaaa and cccc have expired.
task_environment_.FastForwardBy(base::Seconds(1));
EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
EXPECT_TRUE(cache->SearchCache({"cccc"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
EXPECT_TRUE(cache->SearchCache({"aaaa", "cccc"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/2);
}
TEST_F(HashRealTimeCacheTest, TestCacheMatching_ExpirationNanos) {
auto cache = std::make_unique<HashRealTimeCache>();
// The below are done within blocks to ensure that the cache works even once
// the inputs to CacheSearchHashesResponse have been destructed.
{
std::vector<std::string> requested_hash_prefixes = {"aaaa"};
std::vector<V5::FullHash> response_full_hashes = {
CreateBasicFullHash(
"aaaa1111111111111111111111111111",
{V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 500000000));
}
task_environment_.FastForwardBy(base::Seconds(300));
// aaaa expires at 300.5 seconds.
// Current time = 300.0 seconds. aaaa has not expired.
EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
// Current time = 300.5 seconds. aaaa has expired.
task_environment_.FastForwardBy(base::Nanoseconds(500000000));
EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
}
TEST_F(HashRealTimeCacheTest, TestCacheMatching_Attributes) {
auto cache = std::make_unique<HashRealTimeCache>();
// The below is done within a block to ensure that the cache works even once
// the inputs to CacheSearchHashesResponse have been destructed.
{
std::vector<std::string> requested_hash_prefixes = {"aaaa", "bbbb"};
auto full_hash_1 =
CreateBasicFullHash("aaaa1111111111111111111111111111", {});
AddThreatTypeAndAttributes(
full_hash_1, V5::ThreatType::SOCIAL_ENGINEERING,
{V5::ThreatAttribute::CANARY, V5::ThreatAttribute::FRAME_ONLY});
AddThreatTypeAndAttributes(full_hash_1, V5::ThreatType::MALWARE,
{V5::ThreatAttribute::CANARY});
AddThreatTypeAndAttributes(
full_hash_1, V5::ThreatType::API_ABUSE,
{V5::ThreatAttribute::CANARY, V5::ThreatAttribute::FRAME_ONLY});
AddThreatTypeAndAttributes(full_hash_1, V5::ThreatType::UNWANTED_SOFTWARE,
{});
std::vector<V5::FullHash> response_full_hashes = {
full_hash_1, CreateBasicFullHash("aaaa2222222222222222222222222222",
{V5::ThreatType::MALWARE})};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 0));
}
std::set<std::string> hash_prefixes_to_search = {"aaaa", "bbbb"};
auto cache_results = cache->SearchCache(hash_prefixes_to_search);
CheckAndResetCacheHitsAndMisses(/*num_hits=*/2, /*num_misses=*/0);
// Sanity check that adding attributes for aaaa hashes does not change the
// fact that there should be no bbbb full hashes / associated attributes.
EXPECT_TRUE(base::Contains(cache_results, "bbbb"));
EXPECT_TRUE(cache_results["bbbb"].empty());
// We expect aaaa...1 and aaaa...2 both to be in the cache.
EXPECT_EQ(cache_results["aaaa"].size(), 2u);
// aaaa...1 should be filtered down to relevant threat types, meaning some
// attributes get filtered out too since they are associated with a specific
// threat type.
auto aaaa1_results = cache_results["aaaa"][0];
EXPECT_EQ(aaaa1_results.full_hash(), "aaaa1111111111111111111111111111");
auto aaaa1_details = aaaa1_results.full_hash_details();
EXPECT_EQ(aaaa1_details.size(), 3);
EXPECT_EQ(aaaa1_details[0].threat_type(), V5::ThreatType::SOCIAL_ENGINEERING);
EXPECT_EQ(aaaa1_details[0].attributes().size(), 2);
EXPECT_EQ(aaaa1_details[0].attributes()[0], V5::ThreatAttribute::CANARY);
EXPECT_EQ(aaaa1_details[0].attributes()[1], V5::ThreatAttribute::FRAME_ONLY);
EXPECT_EQ(aaaa1_details[1].threat_type(), V5::ThreatType::MALWARE);
EXPECT_EQ(aaaa1_details[1].attributes().size(), 1);
EXPECT_EQ(aaaa1_details[1].attributes()[0], V5::ThreatAttribute::CANARY);
EXPECT_EQ(aaaa1_details[2].threat_type(), V5::ThreatType::UNWANTED_SOFTWARE);
EXPECT_TRUE(aaaa1_details[2].attributes().empty());
// Sanity check that aaaa...2 has no attributes in spite of aaaa...1 having
// attributes.
auto aaaa2_results = cache_results["aaaa"][1];
EXPECT_EQ(aaaa2_results.full_hash(), "aaaa2222222222222222222222222222");
auto aaaa2_details = aaaa2_results.full_hash_details();
EXPECT_EQ(aaaa2_details.size(), 1);
EXPECT_EQ(aaaa2_details[0].threat_type(), V5::ThreatType::MALWARE);
EXPECT_TRUE(aaaa2_details[0].attributes().empty());
}
TEST_F(HashRealTimeCacheTest, TestCacheMatching_OverwrittenEntry) {
auto cache = std::make_unique<HashRealTimeCache>();
// The below are done within blocks to ensure that the cache works even once
// the inputs to CacheSearchHashesResponse have been destructed.
{
// Set up the cache for Request #1.
std::vector<std::string> requested_hash_prefixes = {"aaaa"};
std::vector<V5::FullHash> response_full_hashes = {
CreateBasicFullHash(
"aaaa1111111111111111111111111111",
{V5::ThreatType::SOCIAL_ENGINEERING, V5::ThreatType::MALWARE,
V5::ThreatType::UNWANTED_SOFTWARE, V5::ThreatType::API_ABUSE}),
};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 0));
}
// Confirm the cache has the expected results.
auto cache_results_1 = cache->SearchCache({"aaaa"});
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
EXPECT_EQ(cache_results_1.size(), 1u);
EXPECT_EQ(cache_results_1["aaaa"].size(), 1u);
EXPECT_EQ(cache_results_1["aaaa"][0].full_hash(),
"aaaa1111111111111111111111111111");
EXPECT_EQ(cache_results_1["aaaa"][0].full_hash_details_size(), 3);
{
// Set up the cache for Request #2, overwriting the results of Request #1.
std::vector<std::string> requested_hash_prefixes = {"aaaa"};
std::vector<V5::FullHash> response_full_hashes = {
CreateBasicFullHash("aaaa2222222222222222222222222222",
{V5::ThreatType::MALWARE}),
};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 0));
}
// If there is a race where there are two outgoing hash-prefix real-time
// requests for the same prefix, the later-responding result replaces the
// earlier-responding result. In practice, the two results are expected to be
// the same almost always, but if they are not, this is how the cache behaves.
auto cache_results_2 = cache->SearchCache({"aaaa"});
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
EXPECT_EQ(cache_results_2.size(), 1u);
EXPECT_EQ(cache_results_2["aaaa"].size(), 1u);
EXPECT_EQ(cache_results_2["aaaa"][0].full_hash(),
"aaaa2222222222222222222222222222");
EXPECT_EQ(cache_results_2["aaaa"][0].full_hash_details_size(), 1);
task_environment_.FastForwardBy(base::Seconds(150));
{
// Set up the cache for Request #3, overwriting the results of Request #2.
// The main overwriting here is just the cache duration, since 150 seconds
// have passed.
std::vector<std::string> requested_hash_prefixes = {"aaaa"};
std::vector<V5::FullHash> response_full_hashes = {
CreateBasicFullHash("aaaa2222222222222222222222222222",
{V5::ThreatType::MALWARE}),
};
cache->CacheSearchHashesResponse(requested_hash_prefixes,
response_full_hashes,
CreateCacheDuration(300, 0));
}
// Confirm caching Request #3 overwrote the cache duration. If it didn't, then
// the results of Request #2 would already have expired.
task_environment_.FastForwardBy(base::Seconds(150));
EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
// Confirm Request #3's cache duration is respected.
task_environment_.FastForwardBy(base::Seconds(149));
EXPECT_FALSE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/1, /*num_misses=*/0);
task_environment_.FastForwardBy(base::Seconds(1));
EXPECT_TRUE(cache->SearchCache({"aaaa"}).empty());
CheckAndResetCacheHitsAndMisses(/*num_hits=*/0, /*num_misses=*/1);
}
TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_EmptyCache) {
auto cache = std::make_unique<HashRealTimeCache>();
EXPECT_EQ(GetNumCacheEntries(cache), 0);
cache->ClearExpiredResults();
EXPECT_EQ(GetNumCacheEntries(cache), 0);
}
TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_NoExpiredResults) {
auto cache = std::make_unique<HashRealTimeCache>();
CacheEntry(cache, "aaaa1111111111111111111111111111", 300);
CacheEntry(cache, "cccc1111111111111111111111111111", 500);
EXPECT_EQ(GetNumCacheEntries(cache), 2);
EXPECT_TRUE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
cache->ClearExpiredResults();
EXPECT_EQ(GetNumCacheEntries(cache), 2);
EXPECT_TRUE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
}
TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_OneExpiredResult) {
auto cache = std::make_unique<HashRealTimeCache>();
CacheEntry(cache, "aaaa1111111111111111111111111111", 300);
CacheEntry(cache, "cccc1111111111111111111111111111", 500);
// After 400 seconds, aaaa is expired but not cccc.
task_environment_.FastForwardBy(base::Seconds(400));
EXPECT_EQ(GetNumCacheEntries(cache), 2);
EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
cache->ClearExpiredResults();
EXPECT_EQ(GetNumCacheEntries(cache), 1);
EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
EXPECT_TRUE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
}
TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_SomeExpiredResults) {
auto cache = std::make_unique<HashRealTimeCache>();
auto soon = 300;
auto later = 500;
CacheEntry(cache, "aaaa1111111111111111111111111111", soon);
CacheEntry(cache, "bbbb1111111111111111111111111111", later);
CacheEntry(cache, "cccc1111111111111111111111111111", soon);
CacheEntry(cache, "dddd1111111111111111111111111111", soon);
CacheEntry(cache, "eeee1111111111111111111111111111", soon);
CacheEntry(cache, "ffff1111111111111111111111111111", later);
CacheEntry(cache, "gggg1111111111111111111111111111", later);
CacheEntry(cache, "hhhh1111111111111111111111111111", soon);
auto validate_cache_contents = [](std::unique_ptr<HashRealTimeCache>&
cache_internal) {
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"aaaa"}), "aaaa"));
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"bbbb"}), "bbbb"));
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"cccc"}), "cccc"));
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"dddd"}), "dddd"));
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"eeee"}), "eeee"));
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"ffff"}), "ffff"));
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"gggg"}), "gggg"));
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"hhhh"}), "hhhh"));
};
// After 400 seconds, all of the "soon" prefixes have expired, and none of the
// "later" prefixes have.
task_environment_.FastForwardBy(base::Seconds(400));
EXPECT_EQ(GetNumCacheEntries(cache), 8);
validate_cache_contents(cache);
cache->ClearExpiredResults();
EXPECT_EQ(GetNumCacheEntries(cache), 3);
validate_cache_contents(cache);
}
TEST_F(HashRealTimeCacheTest,
TestClearExpiredResults_SomeExpiredResultsReversed) {
// The main difference between TestClearExpiredResults_SomeExpiredResults
// above and this one is that whether an entry is expired is reversed. This is
// to confirm that the iterative deletion in ClearExpiredResults works as
// expected regardless of ordering.
auto cache = std::make_unique<HashRealTimeCache>();
auto soon = 300;
auto later = 500;
CacheEntry(cache, "aaaa1111111111111111111111111111", later);
CacheEntry(cache, "bbbb1111111111111111111111111111", soon);
CacheEntry(cache, "cccc1111111111111111111111111111", later);
CacheEntry(cache, "dddd1111111111111111111111111111", later);
CacheEntry(cache, "eeee1111111111111111111111111111", later);
CacheEntry(cache, "ffff1111111111111111111111111111", soon);
CacheEntry(cache, "gggg1111111111111111111111111111", soon);
CacheEntry(cache, "hhhh1111111111111111111111111111", later);
auto validate_cache_contents = [](std::unique_ptr<HashRealTimeCache>&
cache_internal) {
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"aaaa"}), "aaaa"));
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"bbbb"}), "bbbb"));
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"cccc"}), "cccc"));
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"dddd"}), "dddd"));
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"eeee"}), "eeee"));
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"ffff"}), "ffff"));
EXPECT_FALSE(base::Contains(cache_internal->SearchCache({"gggg"}), "gggg"));
EXPECT_TRUE(base::Contains(cache_internal->SearchCache({"hhhh"}), "hhhh"));
};
// After 400 seconds, all of the "soon" prefixes have expired, and none of the
// "later" prefixes have.
task_environment_.FastForwardBy(base::Seconds(400));
EXPECT_EQ(GetNumCacheEntries(cache), 8);
validate_cache_contents(cache);
cache->ClearExpiredResults();
EXPECT_EQ(GetNumCacheEntries(cache), 5);
validate_cache_contents(cache);
}
TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_AllExpiredResults) {
auto cache = std::make_unique<HashRealTimeCache>();
CacheEntry(cache, "aaaa1111111111111111111111111111", 300);
CacheEntry(cache, "cccc1111111111111111111111111111", 500);
// After 500 seconds, both have expired.
task_environment_.FastForwardBy(base::Seconds(500));
EXPECT_EQ(GetNumCacheEntries(cache), 2);
EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
EXPECT_FALSE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
cache->ClearExpiredResults();
EXPECT_EQ(GetNumCacheEntries(cache), 0);
EXPECT_FALSE(base::Contains(cache->SearchCache({"aaaa"}), "aaaa"));
EXPECT_FALSE(base::Contains(cache->SearchCache({"cccc"}), "cccc"));
}
TEST_F(HashRealTimeCacheTest, TestClearExpiredResults_Logging) {
auto cache = std::make_unique<HashRealTimeCache>();
// Cache is empty.
cache->ClearExpiredResults();
CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/0, /*num_full_hashes=*/0);
// Cache has 1 hash prefix with 1 full hash in it.
cache->CacheSearchHashesResponse(
{"aaaa"},
{CreateBasicFullHash("aaaa1111111111111111111111111111",
{V5::ThreatType::MALWARE})},
CreateCacheDuration(300, 0));
cache->ClearExpiredResults();
CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/1, /*num_full_hashes=*/1);
// Cache has 2 hash prefixes and 3 full hashes (aaaa entry from above remains
// included).
cache->CacheSearchHashesResponse(
{"bbbb"},
{CreateBasicFullHash("bbbb1111111111111111111111111111",
{V5::ThreatType::MALWARE}),
CreateBasicFullHash("bbbb2222222222222222222222222222",
{V5::ThreatType::MALWARE})},
CreateCacheDuration(500, 0));
cache->ClearExpiredResults();
CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/2, /*num_full_hashes=*/3);
// 400 seconds later, the first addition to the cache has expired. The logs
// should still report 2 hash prefixes and 3 full hashes, because they report
// the size at the time the cache started being cleared, not afterwards.
task_environment_.FastForwardBy(base::Seconds(400));
cache->ClearExpiredResults();
CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/2, /*num_full_hashes=*/3);
// Clearing the expired results again now displays the size with just the
// second addition to the cache.
cache->ClearExpiredResults();
CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/1, /*num_full_hashes=*/2);
// 100 seconds later, the second addition to the cache has expired. The log
// still includes it in the size (same rationale as above).
task_environment_.FastForwardBy(base::Seconds(100));
cache->ClearExpiredResults();
CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/1, /*num_full_hashes=*/2);
// Clearing the expired results again now logs that the cache is empty.
cache->ClearExpiredResults();
CheckAndResetCacheSizeOnClear(/*num_hash_prefixes=*/0, /*num_full_hashes=*/0);
}
} // namespace safe_browsing