blob: e99ed73f969cc63be495b456939c0e4a1a6b26e1 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/http/no_vary_search_cache.h"
#include <algorithm>
#include <array>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/pickle.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "net/base/features.h"
#include "net/base/load_flags.h"
#include "net/base/network_isolation_key.h"
#include "net/base/pickle.h"
#include "net/base/pickle_traits.h"
#include "net/base/schemeful_site.h"
#include "net/http/http_cache.h"
#include "net/http/http_response_headers.h"
#include "net/http/no_vary_search_cache_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace net {
namespace {
namespace nvs_test = no_vary_search_cache_test_utils;
using ::testing::_;
using ::testing::AllOf;
using ::testing::EndsWith;
using ::testing::Eq;
using ::testing::Ge;
using ::testing::InSequence;
using ::testing::Le;
using ::testing::Optional;
constexpr size_t kMaxSize = 5;
class NoVarySearchCacheTest : public ::testing::TestWithParam<bool> {
public:
NoVarySearchCacheTest() {
feature_list_.InitWithFeatureState(
features::kSplitCacheByNetworkIsolationKey, GetParam());
}
protected:
NoVarySearchCache& cache() { return cache_; }
// Generates a URL with the query `query`.
static GURL TestURL(std::string_view query = {}) {
return nvs_test::CreateTestURL(query);
}
// Generates an HttpRequestInfo object containing a URL that has the query
// `query`.
static HttpRequestInfo TestRequest(std::string_view query = {}) {
return nvs_test::CreateTestRequest(query);
}
// Generates an HttpRequestInfo object with the URL `url`.
static HttpRequestInfo TestRequest(const GURL& url) {
return nvs_test::CreateTestRequest(url);
}
// Generates an HttpRequestInfo object with the given `url` and `nik`.
static HttpRequestInfo TestRequest(const GURL& url,
const NetworkIsolationKey& nik) {
return nvs_test::CreateTestRequest(url, nik);
}
// Returns a reference to an HttpResponseHeaders object with a No-Vary-Search
// header with the value `no_vary_search`.
const HttpResponseHeaders& TestHeaders(std::string_view no_vary_search) {
response_header_storage_.push_back(
nvs_test::CreateTestHeaders(no_vary_search));
return *response_header_storage_.back();
}
// Inserts a URL with query `query` into cache with a No-Vary-Search header
// value of `no_vary_search`.
void Insert(std::string_view query, std::string_view no_vary_search) {
nvs_test::Insert(cache_, query, no_vary_search);
}
// Returns true if TestURL(query) exists in cache.
bool Exists(std::string_view query) {
return nvs_test::Exists(cache_, query);
}
// Returns true if inserting a request with query `insert` results in a lookup
// for query `lookup` succeeding, assuming a No-Vary-Search header value of
// `no_vary_search`.
bool Matches(std::string_view insert,
std::string_view lookup,
std::string_view no_vary_search) {
NoVarySearchCache cache(kMaxSize);
cache.MaybeInsert(TestRequest(insert), TestHeaders(no_vary_search));
EXPECT_EQ(cache.size(), 1u);
const auto exists = [&cache](std::string_view query) {
return cache.Lookup(TestRequest(query)).has_value();
};
// It would be bad if the query didn't match itself.
EXPECT_TRUE(exists(insert));
return exists(lookup);
}
// Returns true if inserting a request with query `insert2` after a request
// for `insert1` results in the new request replacing the old one, meaning
// they were match according to the No-Vary-Search header value of
// `no_vary_search`.
bool InsertMatches(std::string_view insert1,
std::string_view insert2,
std::string_view no_vary_search) {
NoVarySearchCache cache(kMaxSize);
const auto insert = [&cache, no_vary_search, this](std::string_view query) {
cache.MaybeInsert(TestRequest(query), TestHeaders(no_vary_search));
};
insert(insert1);
EXPECT_EQ(cache.size(), 1u);
insert(insert2);
return cache.size() == 1u;
}
std::string GenerateCachePartitionKey(std::string_view url) {
const auto request = TestRequest(GURL(url));
std::optional<std::string> maybe_cache_key =
HttpCache::GenerateCachePartitionKeyForRequest(request);
EXPECT_TRUE(maybe_cache_key);
return maybe_cache_key.value_or(std::string());
}
private:
base::test::ScopedFeatureList feature_list_;
NoVarySearchCache cache_{kMaxSize};
// Scratch space for HttpResponseHeaders objects so that TestHeaders() can
// return a const reference for convenience. This is thrown away at the end of
// each test.
std::vector<scoped_refptr<HttpResponseHeaders>> response_header_storage_;
};
INSTANTIATE_TEST_SUITE_P(All,
NoVarySearchCacheTest,
::testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "SplitCacheEnabled"
: "SplitCacheDisabled";
});
TEST_P(NoVarySearchCacheTest, NewlyConstructedCacheIsEmpty) {
EXPECT_EQ(cache().size(), 0u);
}
TEST_P(NoVarySearchCacheTest, LookupOnEmptyCache) {
EXPECT_EQ(cache().Lookup(TestRequest()), std::nullopt);
}
TEST_P(NoVarySearchCacheTest, InsertLookupErase) {
Insert("", "key-order");
auto result = cache().Lookup(TestRequest());
ASSERT_TRUE(result);
EXPECT_EQ(result->original_url, TestURL());
EXPECT_EQ(cache().size(), 1u);
cache().Erase(std::move(result->erase_handle));
EXPECT_EQ(cache().size(), 0u);
EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}
TEST_P(NoVarySearchCacheTest, MoveConstruct) {
Insert("a=b", "key-order");
NoVarySearchCache new_cache = std::move(cache());
EXPECT_TRUE(new_cache.Lookup(TestRequest("a=b")));
// NOLINTNEXTLINE(bugprone-use-after-move)
EXPECT_EQ(cache().size(), 0u);
}
// An asan build will find leaks, but this test works on any build.
TEST_P(NoVarySearchCacheTest, QueryNotLeaked) {
std::optional<NoVarySearchCache::LookupResult> result;
{
NoVarySearchCache cache(kMaxSize);
cache.MaybeInsert(TestRequest(), TestHeaders("params"));
result = cache.Lookup(TestRequest());
ASSERT_TRUE(result);
EXPECT_FALSE(result->erase_handle.IsGoneForTesting());
}
EXPECT_TRUE(result->erase_handle.IsGoneForTesting());
}
std::string QueryWithIParameter(size_t i) {
return "i=" + base::NumberToString(i);
}
constexpr std::string_view kVaryOnIParameter = "params, except=(\"i\")";
TEST_P(NoVarySearchCacheTest, OldestItemIsEvicted) {
for (size_t i = 0; i < kMaxSize + 1; ++i) {
std::string query = QueryWithIParameter(i);
Insert(query, kVaryOnIParameter);
EXPECT_TRUE(Exists(query));
}
EXPECT_EQ(cache().size(), kMaxSize);
EXPECT_FALSE(Exists("i=0"));
}
TEST_P(NoVarySearchCacheTest, RecentlyUsedItemIsNotEvicted) {
for (size_t i = 0; i < kMaxSize + 1; ++i) {
std::string query = QueryWithIParameter(i);
Insert(query, kVaryOnIParameter);
EXPECT_TRUE(Exists(query));
// Exists() calls Lookup(), which makes an entry "used".
EXPECT_TRUE(Exists("i=0"));
}
EXPECT_EQ(cache().size(), kMaxSize);
EXPECT_TRUE(Exists("i=0"));
EXPECT_FALSE(Exists("i=1"));
}
TEST_P(NoVarySearchCacheTest, MostRecentlyUsedItemIsNotEvicted) {
const auto query = QueryWithIParameter;
// Fill the cache.
for (size_t i = 0; i < kMaxSize; ++i) {
Insert(query(i), kVaryOnIParameter);
}
EXPECT_EQ(cache().size(), kMaxSize);
// Make "i=3" be the most recently used item.
EXPECT_TRUE(Exists("i=3"));
// Evict kMaxSize - 1 items.
for (size_t i = kMaxSize; i < kMaxSize * 2 - 1; ++i) {
Insert(query(i), kVaryOnIParameter);
EXPECT_TRUE(Exists(query(i)));
}
EXPECT_EQ(cache().size(), kMaxSize);
EXPECT_TRUE(Exists("i=3"));
}
TEST_P(NoVarySearchCacheTest, LeastRecentlyUsedItemIsEvicted) {
const auto query = QueryWithIParameter;
// Fill the cache.
for (size_t i = 0; i < kMaxSize; ++i) {
Insert(query(i), kVaryOnIParameter);
}
EXPECT_EQ(cache().size(), kMaxSize);
// Make "i=kMaxSize-1" be the least recently used item.
for (size_t i = 0; i < kMaxSize - 1; ++i) {
EXPECT_TRUE(Exists(query(i)));
}
// Evict one item.
Insert(query(kMaxSize), kVaryOnIParameter);
// Verify it was the least-recently-used item.
EXPECT_FALSE(Exists(query(kMaxSize - 1)));
}
TEST_P(NoVarySearchCacheTest, InsertUpdatesIdenticalItem) {
Insert("a=b", "params=(\"c\")");
auto original_result = cache().Lookup(TestRequest("a=b"));
ASSERT_TRUE(original_result);
Insert("a=b", "params=(\"c\")");
auto new_result = cache().Lookup(TestRequest("a=b"));
ASSERT_TRUE(new_result);
EXPECT_TRUE(
original_result->erase_handle.EqualsForTesting(new_result->erase_handle));
}
TEST_P(NoVarySearchCacheTest, InsertRemovesMatchingItem) {
Insert("a=b&c=1", "params=(\"c\")");
auto original_result = cache().Lookup(TestRequest("a=b"));
ASSERT_TRUE(original_result);
EXPECT_EQ(original_result->original_url, TestURL("a=b&c=1"));
Insert("a=b&c=2", "params=(\"c\")");
EXPECT_TRUE(original_result->erase_handle.IsGoneForTesting());
EXPECT_EQ(cache().size(), 1u);
auto new_result = cache().Lookup(TestRequest("a=b"));
EXPECT_EQ(new_result->original_url, TestURL("a=b&c=2"));
}
TEST_P(NoVarySearchCacheTest, MaybeInsertDoesNothingWithNoNoVarySearchHeader) {
auto headers = HttpResponseHeaders::Builder({1, 1}, "200 OK").Build();
cache().MaybeInsert(TestRequest(), *headers);
EXPECT_EQ(cache().size(), 0u);
EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}
TEST_P(NoVarySearchCacheTest, MaybeInsertDoesNothingForDefaultBehavior) {
// The following header values are all equivalent to default behavior.
static constexpr auto kDefaultCases = std::to_array<std::string_view>(
{"", " ", "params=?0", "params=()", "key-order=?0", "nonsense"});
for (auto no_vary_search : kDefaultCases) {
NoVarySearchCache cache(kMaxSize);
Insert("a=b", no_vary_search);
EXPECT_EQ(cache.size(), 0u) << no_vary_search;
}
}
// A size 1 cache is a special case, because eviction results in an empty cache.
TEST_P(NoVarySearchCacheTest, EvictWithSize1Cache) {
NoVarySearchCache cache(1u);
cache.MaybeInsert(TestRequest("a=1"), TestHeaders("key-order"));
cache.MaybeInsert(TestRequest("a=2"), TestHeaders("key-order"));
EXPECT_TRUE(cache.Lookup(TestRequest("a=2")).has_value());
EXPECT_EQ(cache.size(), 1u);
}
// This is a regression test for a bug where insertion led to eviction of the
// only cache entry with the same base URL.
TEST_P(NoVarySearchCacheTest, InsertWithBaseURLMatchingEvicted) {
static constexpr auto my_test_request = [](std::string_view query) {
constexpr std::string_view kBaseURL = "https://a.example/?";
return TestRequest(GURL(base::StrCat({kBaseURL, query})));
};
cache().MaybeInsert(my_test_request("will-be-evicted"),
TestHeaders("key-order"));
for (size_t i = 1; i < kMaxSize; ++i) {
Insert(QueryWithIParameter(i), kVaryOnIParameter);
}
EXPECT_EQ(cache().size(), kMaxSize);
cache().MaybeInsert(my_test_request("same-base-url"),
TestHeaders("key-order"));
EXPECT_TRUE(cache().Lookup(my_test_request("same-base-url")).has_value());
}
// This is a regression test for a bug where insertion led to eviction of the
// only cache entry with the same base URL & No-Vary-Search header value.
// The difference from the InsertWithBaseURLMatchingEvicted test is that in this
// test there are other entries with the same base URL.
TEST_P(NoVarySearchCacheTest, InsertWithNoVarySearchValueMatchingEvicted) {
Insert("will-be-evicted", "params=(\"ignored\")");
for (size_t i = 1; i < kMaxSize; ++i) {
Insert(QueryWithIParameter(i), kVaryOnIParameter);
}
EXPECT_EQ(cache().size(), kMaxSize);
Insert("same-nvs", "params=(\"ignored\")");
EXPECT_TRUE(Exists("same-nvs"));
}
TEST_P(NoVarySearchCacheTest, MatchesWithoutQueryString) {
auto url_with_query = GURL("https://example.com/foo?");
cache().MaybeInsert(TestRequest(url_with_query), TestHeaders("key-order"));
auto result = cache().Lookup(TestRequest(GURL("https://example.com/foo")));
ASSERT_TRUE(result);
EXPECT_EQ(result->original_url, url_with_query);
}
TEST_P(NoVarySearchCacheTest, InsertInvalidURLIsIgnored) {
auto invalid_url = GURL("???");
ASSERT_FALSE(invalid_url.is_valid());
cache().MaybeInsert(TestRequest(invalid_url), TestHeaders("key-order"));
EXPECT_EQ(cache().size(), 0u);
}
// There's no way to insert an invalid URL into the cache. There's also no way
// to add a query to a valid URL to make it invalid. So this test just verifies
// that we don't crash.
TEST_P(NoVarySearchCacheTest, LookupInvalidURLReturnsNullopt) {
GURL invalid_url = GURL("???");
ASSERT_FALSE(invalid_url.is_valid());
auto result = cache().Lookup(TestRequest(invalid_url));
EXPECT_FALSE(result);
}
TEST_P(NoVarySearchCacheTest, MatchCases) {
struct Case {
std::string_view description;
std::string_view query1;
std::string_view query2;
std::string_view no_vary_search;
};
static constexpr Case cases[] = {
{"Encoded & in key", "%26=a&b=c", "b=c", "params=(\"&\")"},
{"Encoded & in value", "a=b%26&c=d", "c=d&a=b%26", "key-order"},
{"Encoded =", "%3d=a", "%3D=a", "key-order"},
{"Encoded and unencoded =", "a=%3d", "a==", "key-order"},
{"Embedded null in key", "a%00b=c", "", "params=(\"a%00b\")"},
{"Embedded null in value", "a=b%00c", "a=b%00c", "key-order"},
{"Encoded space in key", "+=a", "%20=b", "params=(\" \")"},
{"Encoded space in value", "a=b&c=+", "c=%20&a=b", "key-order"},
{"Key is ?", "?=1", "", "params=(\"?\")"},
{"Empty key", "=7&c=d", "c=d", "params=(\"\")"},
{"Empty value", "a=&c=d", "c=d&a", "key-order"},
{"Bad UTF8", "%fe=%ff", "\xfe=\xff", "key-order"},
{"Two params removed", "a=b&c=d&e", "e", R"(params=("a" "c"))"},
};
for (const auto& [description, query1, query2, no_vary_search] : cases) {
EXPECT_TRUE(Matches(query1, query2, no_vary_search))
<< "Testing forwards: " << description;
EXPECT_TRUE(Matches(query2, query1, no_vary_search))
<< "Testing backwards: " << description;
EXPECT_TRUE(InsertMatches(query1, query2, no_vary_search))
<< "Testing double insert: " << description;
}
}
TEST_P(NoVarySearchCacheTest, NoMatchCases) {
struct Case {
std::string_view description;
std::string_view query1;
std::string_view query2;
};
static constexpr Case cases[] = {
{"Encoded &", "a&b", "a%26b"},
{"Encoded =", "a=b", "a%3db"},
};
static constexpr std::string_view kKeyOrder = "key-order";
for (const auto& [description, query1, query2] : cases) {
EXPECT_FALSE(Matches(query1, query2, kKeyOrder))
<< "Testing forwards: " << description;
EXPECT_FALSE(Matches(query2, query1, kKeyOrder))
<< "Testing backwards: " << description;
EXPECT_FALSE(InsertMatches(query1, query2, kKeyOrder))
<< "Testing double insert: " << description;
}
}
// Different representations of No-Very-Search headers that should compare
// equal.
TEST_P(NoVarySearchCacheTest, NoVarySearchVariants) {
struct Case {
std::string_view description;
std::string_view variant1;
std::string_view variant2;
};
static constexpr Case cases[] = {
{"Extra space", R"(params=("a" "b"))", R"(params=( "a" "b" ))"},
{"Bool or omitted params", "params", "params=?1"},
{"Bool or omitted key-order", "key-order", "key-order=?1"},
{"Absent or false key-order", "params, key-order=?0", "params"},
{"Ignored entry", "params, ignored", "params"},
{"Empty except", "params, except=()", "except=(), params"},
{"Different order", R"(params, except=("a"), key-order)",
R"(key-order, except=("a"), params)"},
};
static constexpr std::string_view kQuery = "a=b&b=7&c=d";
for (const auto& [description, variant1, variant2] : cases) {
NoVarySearchCache cache(kMaxSize);
cache.MaybeInsert(TestRequest(kQuery), TestHeaders(variant1));
EXPECT_EQ(cache.size(), 1u);
cache.MaybeInsert(TestRequest(kQuery), TestHeaders(variant2));
EXPECT_EQ(cache.size(), 1u)
<< "Failing: " << description << "; variant1='" << variant1
<< "'; variant2 = '" << variant2 << "'";
}
}
// Items with a transient NIK will not be stored in the disk cache, and so they
// shouldn't be stored in the NoVarySearchCache either.
TEST_P(NoVarySearchCacheTest, TransientNIK) {
const auto transient = NetworkIsolationKey::CreateTransientForTesting();
cache().MaybeInsert(TestRequest(TestURL(), transient), TestHeaders("params"));
if (HttpCache::IsSplitCacheEnabled()) {
EXPECT_EQ(cache().size(), 0u);
EXPECT_FALSE(cache().Lookup(TestRequest(TestURL(), transient)));
} else {
EXPECT_EQ(cache().size(), 1u);
EXPECT_TRUE(cache().Lookup(TestRequest(TestURL(), transient)));
}
}
TEST_P(NoVarySearchCacheTest, DifferentNIK) {
const NetworkIsolationKey different_nik(
SchemefulSite(TestURL()),
SchemefulSite(GURL("https://thirdparty.example/")));
cache().MaybeInsert(TestRequest(), TestHeaders("params"));
cache().MaybeInsert(TestRequest(TestURL(), different_nik),
TestHeaders("params"));
const auto result1 = cache().Lookup(TestRequest());
const auto result2 = cache().Lookup(TestRequest(TestURL(), different_nik));
ASSERT_TRUE(result1);
ASSERT_TRUE(result2);
const size_t cache_size = cache().size();
const bool handles_are_equal =
result1->erase_handle.EqualsForTesting(result2->erase_handle);
if (HttpCache::IsSplitCacheEnabled()) {
EXPECT_EQ(cache_size, 2u);
EXPECT_FALSE(handles_are_equal);
} else {
EXPECT_EQ(cache_size, 1u);
EXPECT_TRUE(handles_are_equal);
}
}
TEST_P(NoVarySearchCacheTest, DifferentURL) {
const GURL url1("https://example.com/a?a=b");
const GURL url2("https://example.com/b?a=b");
cache().MaybeInsert(TestRequest(url1), TestHeaders("key-order"));
cache().MaybeInsert(TestRequest(url2), TestHeaders("key-order"));
EXPECT_EQ(cache().size(), 2u);
const auto result1 = cache().Lookup(TestRequest(url1));
const auto result2 = cache().Lookup(TestRequest(url2));
ASSERT_TRUE(result1);
ASSERT_TRUE(result2);
EXPECT_FALSE(result1->erase_handle.EqualsForTesting(result2->erase_handle));
}
void SpinUntilCurrentTimeChanges() {
const auto start = base::Time::Now();
while (start == base::Time::Now()) {
base::PlatformThread::YieldCurrentThread();
}
}
TEST_P(NoVarySearchCacheTest, DifferentNoVarySearch) {
Insert("a=b&c=d", "params, except=(\"a\")");
// Make sure that the two inserts reliably get a different `inserted`
// timestamp so that the ordering is deterministic.
SpinUntilCurrentTimeChanges();
Insert("a=b", "key-order");
EXPECT_EQ(cache().size(), 2u);
const auto result = cache().Lookup(TestRequest("a=b"));
ASSERT_TRUE(result);
// If time goes backwards this test will flake.
EXPECT_EQ(result->original_url, TestURL("a=b"));
}
// The winner of the lookup should depend only on insertion order and not on the
// order of iteration of the map. To ensure this works for any iteration order,
// we perform the same test in the opposite direction.
TEST_P(NoVarySearchCacheTest, DifferentNoVarySearchReverseOrder) {
Insert("a=b", "key-order");
SpinUntilCurrentTimeChanges();
Insert("a=b&c=d", "params, except=(\"a\")");
EXPECT_EQ(cache().size(), 2u);
const auto result = cache().Lookup(TestRequest("a=b"));
ASSERT_TRUE(result);
// If time goes backwards this test will flake.
EXPECT_EQ(result->original_url, TestURL("a=b&c=d"));
}
TEST_P(NoVarySearchCacheTest, EraseInDifferentOrder) {
// Insert in order a, b, c.
Insert("a", "key-order");
Insert("b", "key-order");
Insert("c", "key-order");
// Look up in order b, c, a.
const auto lookup = [this](std::string_view query) {
return cache().Lookup(TestRequest(query));
};
auto result_b = lookup("b");
auto result_c = lookup("c");
auto result_a = lookup("a");
ASSERT_TRUE(result_a);
ASSERT_TRUE(result_b);
ASSERT_TRUE(result_c);
// Erase in order c, a, b.
cache().Erase(std::move(result_c->erase_handle));
EXPECT_TRUE(Exists("a"));
EXPECT_TRUE(Exists("b"));
EXPECT_FALSE(Exists("c"));
cache().Erase(std::move(result_a->erase_handle));
EXPECT_FALSE(Exists("a"));
EXPECT_TRUE(Exists("b"));
cache().Erase(std::move(result_b->erase_handle));
EXPECT_FALSE(Exists("b"));
EXPECT_EQ(cache().size(), 0u);
EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}
// The URL "ref", also known as the "fragment", also known as the "hash", is
// ignored for matching and not stored in the cache.
TEST_P(NoVarySearchCacheTest, URLRefIsIgnored) {
cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b#foo")),
TestHeaders("key-order"));
cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b#bar")),
TestHeaders("key-order"));
EXPECT_EQ(cache().size(), 1u);
auto result =
cache().Lookup(TestRequest(GURL("https://example.com/?a=b#baz")));
EXPECT_TRUE(result);
EXPECT_EQ(result->original_url, GURL("https://example.com/?a=b"));
}
TEST_P(NoVarySearchCacheTest, URLWithUsernameIsRejected) {
const GURL url_with_username("https://me@example.com/?a=b");
cache().MaybeInsert(TestRequest(url_with_username), TestHeaders("key-order"));
EXPECT_EQ(cache().size(), 0u);
// See if it matches against the URL without the username.
cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b")),
TestHeaders("key-order"));
EXPECT_FALSE(cache().Lookup(TestRequest(url_with_username)));
}
TEST_P(NoVarySearchCacheTest, URLWithPasswordIsRejected) {
const GURL url_with_password("https://:hunter123@example.com/?a=b");
cache().MaybeInsert(TestRequest(url_with_password), TestHeaders("key-order"));
EXPECT_EQ(cache().size(), 0u);
// See if it matches against the URL without the password.
cache().MaybeInsert(TestRequest(GURL("https://example.com/?a=b")),
TestHeaders("key-order"));
EXPECT_FALSE(cache().Lookup(TestRequest(url_with_password)));
}
TEST_P(NoVarySearchCacheTest, ClearDataEverything) {
cache().MaybeInsert(TestRequest(GURL("http://example/q?a=b")),
TestHeaders("key-order"));
const NetworkIsolationKey different_nik(
SchemefulSite(TestURL()),
SchemefulSite(GURL("https://thirdparty.example/")));
cache().MaybeInsert(TestRequest(GURL("http://example/q?a=b"), different_nik),
TestHeaders("key-order"));
cache().MaybeInsert(TestRequest(GURL("https://example.com/search?q=z")),
TestHeaders("key-order"));
const bool cleared = cache().ClearData(UrlFilterType::kFalseIfMatches, {}, {},
base::Time(), base::Time::Max());
EXPECT_TRUE(cleared);
EXPECT_EQ(cache().size(), 0u);
EXPECT_TRUE(cache().IsTopLevelMapEmptyForTesting());
}
TEST_P(NoVarySearchCacheTest, ClearDataMatchOrigin) {
// Scheme differs.
cache().MaybeInsert(TestRequest(GURL("https://example.com/q?a=b")),
TestHeaders("key-order"));
cache().MaybeInsert(TestRequest(GURL("http://example.com/q?a=b")),
TestHeaders("key-order"));
const bool cleared =
cache().ClearData(UrlFilterType::kTrueIfMatches,
{url::Origin::Create(GURL("https://example.com/"))}, {},
base::Time(), base::Time::Max());
EXPECT_TRUE(cleared);
EXPECT_EQ(cache().size(), 1u);
EXPECT_TRUE(cache()
.Lookup(TestRequest(GURL("http://example.com/q?a=b")))
.has_value());
}
TEST_P(NoVarySearchCacheTest, ClearDataMatchDomain) {
// Scheme differs.
cache().MaybeInsert(TestRequest(GURL("http://example.com:80/q?a=b")),
TestHeaders("key-order"));
cache().MaybeInsert(TestRequest(GURL("https://www.example.com:8080/q?a=b")),
TestHeaders("key-order"));
cache().MaybeInsert(TestRequest(GURL("https://other.example/q?a=b")),
TestHeaders("key-order"));
const bool cleared =
cache().ClearData(UrlFilterType::kTrueIfMatches, {}, {"example.com"},
base::Time(), base::Time::Max());
EXPECT_TRUE(cleared);
EXPECT_EQ(cache().size(), 1u);
EXPECT_TRUE(cache()
.Lookup(TestRequest(GURL("https://other.example/q?a=b")))
.has_value());
}
TEST_P(NoVarySearchCacheTest, ClearDataMatchTime) {
base::test::TaskEnvironment task_environment(
base::test::SingleThreadTaskEnvironment::TimeSource::MOCK_TIME);
// Function to convert a time string in the format HH:MM:SS to a base::Time.
static constexpr auto time = [](std::string_view time) {
base::Time output;
const bool parsed = base::Time::FromUTCString(
base::StrCat({"Tue, 4 Mar 2025 ", time, " GMT"}).c_str(), &output);
CHECK(parsed);
return output;
};
// Advance the mock time to `when`, specified in the format HH:MM:SS.
const auto advance_time_to = [&](std::string_view when) {
base::Time target_time = time(when);
auto mock_now = base::Time::Now();
CHECK_GT(target_time, mock_now);
task_environment.FastForwardBy(target_time - mock_now);
};
advance_time_to("12:00:00");
Insert("a=1", "key-order");
advance_time_to("13:00:00");
Insert("a=2", "key-order");
advance_time_to("14:00:00");
Insert("a=3", "key-order");
const bool cleared = cache().ClearData(UrlFilterType::kFalseIfMatches, {}, {},
time("12:30:00"), time("13:30:00"));
EXPECT_TRUE(cleared);
EXPECT_EQ(cache().size(), 2u);
EXPECT_TRUE(Exists("a=1"));
EXPECT_FALSE(Exists("a=2"));
EXPECT_TRUE(Exists("a=3"));
}
TEST_P(NoVarySearchCacheTest, ClearDataEmptyCache) {
const bool cleared =
cache().ClearData(UrlFilterType::kTrueIfMatches,
{url::Origin::Create(GURL("https://example.com/"))}, {},
base::Time(), base::Time::Max());
EXPECT_FALSE(cleared);
EXPECT_EQ(cache().size(), 0u);
}
TEST_P(NoVarySearchCacheTest, ClearDataNoMatch) {
Insert("a=1", "key-order");
const bool cleared = cache().ClearData(
UrlFilterType::kTrueIfMatches,
{url::Origin::Create(GURL("https://nomatch.com:9999/"))}, {},
base::Time(), base::Time::Max());
EXPECT_FALSE(cleared);
EXPECT_EQ(cache().size(), 1u);
EXPECT_TRUE(Exists("a=1"));
}
std::optional<NoVarySearchCache> TestPickleRoundTrip(
const NoVarySearchCache& cache) {
base::Pickle pickle;
WriteToPickle(pickle, cache);
// The estimate of PickleSize should always be correct.
EXPECT_EQ(EstimatePickleSize(cache), pickle.payload_size());
auto maybe_cache = ReadValueFromPickle<NoVarySearchCache>(pickle);
if (!maybe_cache) {
return std::nullopt;
}
EXPECT_EQ(cache.size(), maybe_cache->size());
return maybe_cache;
}
TEST_P(NoVarySearchCacheTest, SerializeDeserializeEmpty) {
EXPECT_TRUE(TestPickleRoundTrip(cache()));
}
TEST_P(NoVarySearchCacheTest, SerializeDeserializeSimple) {
Insert("b=1", "key-order");
Insert("c&d", "key-order");
Insert("f=3", "params=(\"a\")");
auto new_cache = TestPickleRoundTrip(cache());
ASSERT_TRUE(new_cache);
const auto lookup = [&](std::string_view params) {
return new_cache->Lookup(TestRequest(params));
};
auto maybe_handle1 = lookup("b=1");
auto maybe_handle2 = lookup("d&c");
auto maybe_handle3 = lookup("f=3&a=7");
ASSERT_TRUE(maybe_handle1);
ASSERT_TRUE(maybe_handle2);
ASSERT_TRUE(maybe_handle3);
new_cache->Erase(std::move(maybe_handle1->erase_handle));
new_cache->Erase(std::move(maybe_handle2->erase_handle));
new_cache->Erase(std::move(maybe_handle3->erase_handle));
EXPECT_EQ(new_cache->size(), 0u);
EXPECT_TRUE(new_cache->IsTopLevelMapEmptyForTesting());
}
TEST_P(NoVarySearchCacheTest, SerializeDeserializeFull) {
for (size_t i = 0; i < kMaxSize; ++i) {
Insert(QueryWithIParameter(i), kVaryOnIParameter);
}
auto new_cache = TestPickleRoundTrip(cache());
ASSERT_TRUE(new_cache);
for (size_t i = 0; i < kMaxSize; ++i) {
EXPECT_TRUE(new_cache->Lookup(TestRequest(QueryWithIParameter(i))));
}
}
TEST_P(NoVarySearchCacheTest, DeserializeBadSizes) {
struct TestCase {
std::string_view test_description;
int size;
int max_size;
int map_size;
};
static constexpr auto kTestCases = std::to_array<TestCase>({
{"Negative size", -1, 1, 0},
{"Size larger than max_size", 2, 1, 0},
{"Size bigger than map contents", 1, 1, 0},
{"Negative max_size", 0, -1, 0},
{"Zero max_size", 0, 0, 0},
{"Negative map size", 0, 1, -1},
{"Map size larger than map contents", 0, 1, 1},
});
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.test_description);
base::Pickle pickle;
// This uses the fact that containers use an integer for size.
WriteToPickle(pickle, test_case.size, test_case.max_size,
test_case.map_size);
EXPECT_FALSE(ReadValueFromPickle<NoVarySearchCache>(pickle));
}
}
// A truncated Pickle should never deserialize to a NoVarySearchCache object.
// This tests covers many different checks for bad data during deserialization.
TEST_P(NoVarySearchCacheTest, TruncatedPickle) {
Insert("a=9&b=1", "params=(\"a\")");
Insert("a=8&b=2", "params=(\"a\")");
Insert("f=3", "params, except=(\"f\")");
Insert("", "params, except=(\"f\")");
base::Pickle pickle;
WriteToPickle(pickle, cache());
// Go up in increments of 4 bytes because a Pickle with a size that is not a
// multiple of 4 is invalid in a way that is not interesting to this test.
for (size_t bytes = 4u; bytes < pickle.payload_size(); bytes += 4) {
SCOPED_TRACE(bytes);
base::Pickle truncated;
truncated.WriteBytes(pickle.payload_bytes().first(bytes));
EXPECT_FALSE(ReadValueFromPickle<NoVarySearchCache>(truncated));
}
}
// A Journal that registers and deregisters itself automatically.
class ScopedJournal : public NoVarySearchCache::Journal {
public:
explicit ScopedJournal(NoVarySearchCache& cache) : cache_(cache) {
cache.SetJournal(this);
}
~ScopedJournal() override { cache_->SetJournal(nullptr); }
private:
raw_ref<NoVarySearchCache> cache_;
};
// A Journal object implemented by GoogleMock.
class ScopedMockJournal : public ScopedJournal {
public:
using ScopedJournal::ScopedJournal;
MOCK_METHOD(void,
OnInsert,
(const std::string&,
const std::string&,
const HttpNoVarySearchData&,
const std::optional<std::string>&,
base::Time),
(override));
MOCK_METHOD(void,
OnErase,
(const std::string&,
const std::string&,
const HttpNoVarySearchData&,
const std::optional<std::string>&),
(override));
};
// A matcher which matches No-Vary-Search: key-order
const auto IsKeyOrder =
Eq(HttpNoVarySearchData::CreateFromNoVaryParams({}, false));
TEST_P(NoVarySearchCacheTest, JournalNewInsert) {
ScopedMockJournal journal(cache());
const base::Time now = base::Time::Now();
EXPECT_CALL(journal, OnInsert(_, "https://example.com/", IsKeyOrder,
Optional(Eq("a=0")), Ge(now)));
Insert("a=0", "key-order");
}
TEST_P(NoVarySearchCacheTest, JournalRefresh) {
Insert("a=1", "key-order");
// Start journalling now.
ScopedMockJournal journal(cache());
const base::Time now = base::Time::Now();
EXPECT_CALL(journal, OnInsert(_, "https://example.com/", IsKeyOrder,
Optional(Eq("a=1")), Ge(now)));
Insert("a=1", "key-order");
}
TEST_P(NoVarySearchCacheTest, JournalReplacement) {
Insert("a=2&k=1", "params=(\"k\")");
ScopedMockJournal journal(cache());
const auto params_k =
HttpNoVarySearchData::CreateFromNoVaryParams({"k"}, true);
const base::Time now = base::Time::Now();
EXPECT_CALL(journal, OnInsert(_, "https://example.com/", Eq(params_k),
Optional(Eq("a=2&k=2")), Ge(now)));
EXPECT_CALL(journal, OnErase).Times(0);
// This one replaces the one inserted earlier, but OnErase() is not called to
// reflect that the old one was removed.
Insert("a=2&k=2", "params=(\"k\")");
}
TEST_P(NoVarySearchCacheTest, JournalErase) {
Insert("a=3", "key-order");
auto [original_url, erase_handle] =
cache().Lookup(TestRequest("a=3")).value();
ScopedMockJournal journal(cache());
EXPECT_CALL(journal, OnErase(_, "https://example.com/", IsKeyOrder,
Optional(Eq("a=3"))));
cache().Erase(std::move(erase_handle));
}
TEST_P(NoVarySearchCacheTest, DontJournalEviction) {
ScopedMockJournal journal(cache());
EXPECT_CALL(journal, OnInsert(_, "https://example.com/", _, _, _))
.Times(kMaxSize + 1);
// Eviction does not result in a call to OnErase().
EXPECT_CALL(journal, OnErase).Times(0);
for (size_t i = 0; i < kMaxSize + 1; ++i) {
Insert(QueryWithIParameter(i), "key-order");
}
}
TEST_P(NoVarySearchCacheTest, DontJournalNonInsertion) {
ScopedMockJournal journal(cache());
EXPECT_CALL(journal, OnInsert).Times(0);
// This No-Vary-Search value is equivalent to the default, so doesn't get
// inserted into the cache.
Insert("a=5", "params=()");
}
TEST_P(NoVarySearchCacheTest, DontJournalClearData) {
Insert("a=6", "key-order");
ScopedMockJournal journal(cache());
EXPECT_CALL(journal, OnErase).Times(0);
// Matches everything.
cache().ClearData(UrlFilterType::kFalseIfMatches, {}, {}, base::Time(),
base::Time::Max());
}
TEST_P(NoVarySearchCacheTest, DontJournalLookup) {
Insert("a=6", "key-order");
ScopedMockJournal journal(cache());
EXPECT_CALL(journal, OnInsert).Times(0);
EXPECT_CALL(journal, OnErase).Times(0);
cache().Lookup(TestRequest("a=6"));
}
// A Journal that clones all changes into a target NoVarySearchCache object.
class CloningJournal : public ScopedJournal {
public:
CloningJournal(NoVarySearchCache& source, NoVarySearchCache& target)
: ScopedJournal(source), target_(target) {}
void OnInsert(const std::string& partition_key,
const std::string& base_url,
const HttpNoVarySearchData& nvs_data,
const std::optional<std::string>& query,
base::Time update_time) override {
target_->ReplayInsert(partition_key, base_url, nvs_data, query,
update_time);
}
// Called when an entry is erased by the Erase() method.
void OnErase(const std::string& partition_key,
const std::string& base_url,
const HttpNoVarySearchData& nvs_data,
const std::optional<std::string>& query) override {
target_->ReplayErase(partition_key, base_url, nvs_data, query);
}
private:
raw_ref<NoVarySearchCache> target_;
};
struct CloneMaker {
NoVarySearchCache clone;
CloningJournal journal;
explicit CloneMaker(NoVarySearchCache& source)
: clone(kMaxSize), journal(source, clone) {}
};
class NoVarySearchCacheReplayTest : public NoVarySearchCacheTest {
protected:
struct ReplayTestCase {
std::string_view description;
HttpRequestInfo to_insert;
std::string_view no_vary_search_value;
HttpRequestInfo to_lookup;
};
auto ReplayTestCases() {
return std::to_array<ReplayTestCase>({
{
"Simple key-order",
TestRequest("c=2&a=1"),
"key-order",
TestRequest("a=1&c=2"),
},
{
"Different no-vary-search",
TestRequest("d=3&e=5"),
"params=(\"d\")",
TestRequest("e=5"),
},
{
"Different base URL",
TestRequest(GURL("https://www.example.com/other?c=2&a=1")),
"key-order",
TestRequest(GURL("https://www.example.com/other?a=1&c=2")),
},
{
"Different site",
TestRequest(GURL("https://example.example/?c=2&a=1")),
"key-order",
TestRequest(GURL("https://example.example/?a=1&c=2")),
},
{
"No question mark",
TestRequest(GURL("https://example2.example/")),
"params",
TestRequest(GURL("https://example2.example/?q=ignored")),
},
});
}
};
INSTANTIATE_TEST_SUITE_P(All,
NoVarySearchCacheReplayTest,
::testing::Bool(),
[](const testing::TestParamInfo<bool>& info) {
return info.param ? "SplitCacheEnabled"
: "SplitCacheDisabled";
});
TEST_P(NoVarySearchCacheReplayTest, Inserts) {
const auto test_cases = ReplayTestCases();
CloneMaker maker(cache());
for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
test_cases) {
cache().MaybeInsert(to_insert, TestHeaders(no_vary_search_value));
}
EXPECT_EQ(maker.clone.size(), test_cases.size());
for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
test_cases) {
SCOPED_TRACE(description);
auto source_lookup_result = cache().Lookup(to_lookup);
auto target_lookup_result = maker.clone.Lookup(to_lookup);
ASSERT_TRUE(source_lookup_result);
ASSERT_TRUE(target_lookup_result);
EXPECT_EQ(source_lookup_result->original_url,
target_lookup_result->original_url);
}
}
TEST_P(NoVarySearchCacheReplayTest, Erases) {
const auto test_cases = ReplayTestCases();
CloneMaker maker(cache());
for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
test_cases) {
cache().MaybeInsert(to_insert, TestHeaders(no_vary_search_value));
}
for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
test_cases) {
SCOPED_TRACE(description);
auto source_lookup_result = cache().Lookup(to_lookup);
ASSERT_TRUE(source_lookup_result);
cache().Erase(std::move(source_lookup_result->erase_handle));
EXPECT_FALSE(maker.clone.Lookup(to_lookup));
}
EXPECT_EQ(maker.clone.size(), 0u);
EXPECT_TRUE(maker.clone.IsTopLevelMapEmptyForTesting());
}
TEST_P(NoVarySearchCacheTest, ReplayInsertBadURLs) {
struct TestCase {
std::string_view description;
std::string_view bad_url;
};
static constexpr auto kBadURLs = std::to_array<TestCase>({
{"Invalid URL", ":"},
{"Not canonical", "https://example%2Eexample/"},
{"No path", "what:"},
{"Has username", "https://bob@example.example/"},
{"Has password", "https://:pass@example.example/"},
{"Has query", "https://example.example/?"},
{"Has ref", "https://example.example/#water"},
});
static constexpr std::string_view kRealURL = "https://example.example/test";
const std::string partition_key = GenerateCachePartitionKey(kRealURL);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
const std::optional<std::string> query = "t=1";
const base::Time update_time;
for (const auto& [description, bad_url] : kBadURLs) {
SCOPED_TRACE(description);
cache().ReplayInsert(partition_key, std::string(bad_url), nvs_data, query,
update_time);
EXPECT_EQ(cache().size(), 0u);
}
}
TEST_P(NoVarySearchCacheTest, ReplayInsertBadQuery) {
static constexpr std::string_view kUrl = "https://example.example/";
const std::string partition_key = GenerateCachePartitionKey(kUrl);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
const base::Time update_time;
cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, "t=1#what",
update_time);
EXPECT_EQ(cache().size(), 0u);
}
TEST_P(NoVarySearchCacheTest, ReplayEraseSuccess) {
static constexpr std::string_view kUrl = "https://example.example/";
const std::string partition_key = GenerateCachePartitionKey(kUrl);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
const std::optional<std::string> query = "t=1";
const base::Time update_time;
cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
update_time);
cache().ReplayErase(partition_key, std::string(kUrl), nvs_data, query);
EXPECT_EQ(cache().size(), 0u);
}
TEST_P(NoVarySearchCacheTest, ReplayEraseOnEmptyCache) {
static constexpr std::string_view kUrl = "https://example.example/";
const std::string partition_key = GenerateCachePartitionKey(kUrl);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
cache().ReplayErase(partition_key, std::string(kUrl), nvs_data, "t=1");
EXPECT_EQ(cache().size(), 0u);
}
TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedPartition) {
static constexpr std::string_view kUrl = "https://example.example/";
const std::string partition_key = GenerateCachePartitionKey(kUrl);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
const std::optional<std::string> query = "t=1";
const base::Time update_time;
cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
update_time);
cache().ReplayErase(partition_key + ".", std::string(kUrl), nvs_data, query);
EXPECT_EQ(cache().size(), 1u);
}
TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedBaseUrl) {
static constexpr std::string_view kUrl = "https://example.example/";
const std::string partition_key = GenerateCachePartitionKey(kUrl);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
const std::optional<std::string> query = "t=1";
const base::Time update_time;
cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
update_time);
cache().ReplayErase(partition_key, std::string(kUrl) + ".", nvs_data, query);
EXPECT_EQ(cache().size(), 1u);
}
TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedNVSData) {
static constexpr std::string_view kUrl = "https://example.example/";
const std::string partition_key = GenerateCachePartitionKey(kUrl);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
const std::optional<std::string> query = "t=1";
const base::Time update_time;
cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
update_time);
const auto mismatched_nvs_data =
HttpNoVarySearchData::CreateFromNoVaryParams({"z"}, true);
cache().ReplayErase(partition_key, std::string(kUrl), mismatched_nvs_data,
query);
EXPECT_EQ(cache().size(), 1u);
}
TEST_P(NoVarySearchCacheTest, ReplayEraseMismatchedQuery) {
static constexpr std::string_view kUrl = "https://example.example/";
const std::string partition_key = GenerateCachePartitionKey(kUrl);
const auto nvs_data = HttpNoVarySearchData::CreateFromNoVaryParams({}, false);
const std::optional<std::string> query = "t=1";
const base::Time update_time;
cache().ReplayInsert(partition_key, std::string(kUrl), nvs_data, query,
update_time);
const std::optional<std::string> mismatched_query = "t=2";
cache().ReplayErase(partition_key, std::string(kUrl), nvs_data,
mismatched_query);
EXPECT_EQ(cache().size(), 1u);
}
// This test doesn't actually cover the Replay methods, but uses the same data
// set for convenience.
TEST_P(NoVarySearchCacheReplayTest, MergeFrom) {
const auto test_cases = ReplayTestCases();
const base::Time before_inserts = base::Time::Now();
for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
test_cases) {
cache().MaybeInsert(to_insert, TestHeaders(no_vary_search_value));
}
const base::Time after_inserts = base::Time::Now();
NoVarySearchCache target(kMaxSize);
ScopedMockJournal journal(target);
EXPECT_CALL(journal, OnErase).Times(0);
{
InSequence s;
for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
test_cases) {
auto expected_nvs_data = HttpNoVarySearchData::ParseFromHeaders(
TestHeaders(no_vary_search_value));
const GURL& url = to_insert.url;
std::optional<std::string_view> query;
if (url.has_query()) {
query = url.query();
}
std::string base_url = url.spec();
if (size_t pos = base_url.find('?'); pos != std::string::npos) {
base_url = base_url.substr(0, pos);
}
EXPECT_CALL(journal,
OnInsert(_, Eq(base_url), Eq(expected_nvs_data), Eq(query),
AllOf(Ge(before_inserts), Le(after_inserts))));
}
}
target.MergeFrom(cache());
EXPECT_EQ(cache().size(), target.size());
for (const auto& [description, to_insert, no_vary_search_value, to_lookup] :
test_cases) {
SCOPED_TRACE(description);
auto source_lookup_result = cache().Lookup(to_lookup);
auto target_lookup_result = target.Lookup(to_lookup);
ASSERT_TRUE(source_lookup_result);
ASSERT_TRUE(target_lookup_result);
EXPECT_EQ(source_lookup_result->original_url,
target_lookup_result->original_url);
}
}
TEST_P(NoVarySearchCacheReplayTest, MergeFromTargetQueriesConsideredOlder) {
const auto query = QueryWithIParameter;
NoVarySearchCache target(kMaxSize);
// Fill the target cache.
for (size_t i = 0; i < kMaxSize; ++i) {
target.MaybeInsert(TestRequest(query(i)), TestHeaders(kVaryOnIParameter));
}
// Put one entry in the source cache.
cache().MaybeInsert(TestRequest(query(kMaxSize)),
TestHeaders(kVaryOnIParameter));
target.MergeFrom(cache());
EXPECT_EQ(target.size(), kMaxSize);
// i=0 has been evicted.
EXPECT_FALSE(target.Lookup(TestRequest(query(0u))));
}
TEST_P(NoVarySearchCacheReplayTest, LRUOrderPreserved) {
const auto query = QueryWithIParameter;
NoVarySearchCache target(kMaxSize);
// Fill the source cache.
for (size_t i = 0; i < kMaxSize; ++i) {
Insert(query(i), kVaryOnIParameter);
}
// Make i=1 be most recently used.
EXPECT_TRUE(Exists(query(1u)));
// Merge to target cache.
target.MergeFrom(cache());
int next_i = kMaxSize;
const auto expect_to_evict = [&](size_t i) {
target.MaybeInsert(TestRequest(query(next_i)),
TestHeaders(kVaryOnIParameter));
EXPECT_FALSE(target.Lookup(TestRequest(query(i))));
++next_i;
};
// Evict i=0.
expect_to_evict(0u);
// Evict i=2 to i=kMaxSize-1.
for (size_t i = 2; i < kMaxSize; ++i) {
expect_to_evict(i);
}
// Evict i=1.
expect_to_evict(1u);
}
TEST_P(NoVarySearchCacheTest, SetMaxSizeSame) {
Insert("a=1", "key-order");
Insert("a=2", "key-order");
ASSERT_EQ(cache().size(), 2u);
ASSERT_EQ(cache().max_size(), kMaxSize);
cache().SetMaxSize(kMaxSize);
EXPECT_EQ(cache().size(), 2u);
EXPECT_EQ(cache().max_size(), kMaxSize);
EXPECT_TRUE(Exists("a=1"));
EXPECT_TRUE(Exists("a=2"));
}
TEST_P(NoVarySearchCacheTest, SetMaxSizeSmaller) {
for (size_t i = 0; i < kMaxSize; ++i) {
Insert(QueryWithIParameter(i), "key-order");
}
ASSERT_EQ(cache().size(), kMaxSize);
cache().SetMaxSize(kMaxSize - 2);
EXPECT_EQ(cache().size(), kMaxSize - 2);
EXPECT_EQ(cache().max_size(), kMaxSize - 2);
// The two least recently used items should be evicted.
EXPECT_FALSE(Exists("i=0"));
EXPECT_FALSE(Exists("i=1"));
EXPECT_TRUE(Exists("i=2"));
EXPECT_TRUE(Exists("i=3"));
EXPECT_TRUE(Exists("i=4"));
}
TEST_P(NoVarySearchCacheTest, SetMaxSizeLarger) {
for (size_t i = 0; i < kMaxSize; ++i) {
Insert(QueryWithIParameter(i), "key-order");
}
ASSERT_EQ(cache().size(), kMaxSize);
cache().SetMaxSize(kMaxSize + 2);
EXPECT_EQ(cache().size(), kMaxSize);
EXPECT_EQ(cache().max_size(), kMaxSize + 2);
// All original items should still be there.
for (size_t i = 0; i < kMaxSize; ++i) {
EXPECT_TRUE(Exists(QueryWithIParameter(i)));
}
// Add two more items.
Insert(QueryWithIParameter(kMaxSize), "key-order");
Insert(QueryWithIParameter(kMaxSize + 1), "key-order");
EXPECT_EQ(cache().size(), kMaxSize + 2);
EXPECT_TRUE(Exists(QueryWithIParameter(kMaxSize)));
EXPECT_TRUE(Exists(QueryWithIParameter(kMaxSize + 1)));
}
TEST_P(NoVarySearchCacheTest, SetMaxSizeOnEmptyCache) {
ASSERT_EQ(cache().size(), 0u);
cache().SetMaxSize(kMaxSize + 5);
EXPECT_EQ(cache().size(), 0u);
EXPECT_EQ(cache().max_size(), kMaxSize + 5);
}
// TODO(https://crbug.com/390216627): Test the various experiments that affect
// the cache key and make sure they also affect NoVarySearchCache.
} // namespace
} // namespace net