blob: bac699998eb7f846b29e082e36779454e963115a [file] [log] [blame] [edit]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/prefetch_cache.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "net/base/isolation_info.h"
#include "net/base/network_isolation_key.h"
#include "net/base/schemeful_site.h"
#include "net/cookies/site_for_cookies.h"
#include "services/network/prefetch_url_loader_client.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace network {
namespace {
constexpr size_t kMaxSize = 10;
// These Test* functions return a value of the appropriate type useful for
// testing. By passing in an index, you can make them produce different
// hostnames that are not same-origin with each other.
GURL TestURL(int index = 0) {
return GURL(base::StringPrintf("https://origin%d.example/i.js", index));
}
url::Origin TestOrigin(int index = 0) {
return url::Origin::Create(TestURL(index));
}
net::IsolationInfo TestIsolationInfo(int index = 0) {
return net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, TestOrigin(index),
TestOrigin(index), net::SiteForCookies::FromOrigin(TestOrigin(index)));
}
net::NetworkIsolationKey TestNIK(int index = 0) {
const net::NetworkIsolationKey nik =
TestIsolationInfo(index).network_isolation_key();
return nik;
}
ResourceRequest MakeResourceRequest(GURL url,
net::IsolationInfo isolation_info) {
ResourceRequest request;
request.url = std::move(url);
request.trusted_params.emplace();
request.trusted_params->isolation_info = std::move(isolation_info);
return request;
}
class PrefetchCacheTest : public ::testing::Test {
protected:
PrefetchCacheTest() {
feature_list_.InitAndEnableFeatureWithParameters(
features::kNetworkContextPrefetch,
{{"max_loaders", base::NumberToString(kMaxSize)}});
erase_grace_time_ = base::GetFieldTrialParamByFeatureAsTimeDelta(
features::kNetworkContextPrefetch,
/*name=*/"erase_grace_time",
/*default_value=*/base::Seconds(1));
}
PrefetchCache& cache() { return cache_; }
void FastForwardBy(base::TimeDelta delta) {
task_environment_.FastForwardBy(delta);
}
base::TimeDelta erase_grace_time() const { return erase_grace_time_; }
base::TimeDelta half_grace_time() const { return erase_grace_time_ / 2; }
private:
base::TimeDelta erase_grace_time_;
base::test::ScopedFeatureList feature_list_;
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
PrefetchCache cache_;
};
TEST_F(PrefetchCacheTest, Emplace) {
PrefetchURLLoaderClient* client =
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo()));
ASSERT_TRUE(client);
EXPECT_EQ(client->url(), TestURL());
EXPECT_EQ(client->network_isolation_key(), TestNIK());
}
TEST_F(PrefetchCacheTest, EmplaceNoNIK) {
ResourceRequest request;
request.url = TestURL();
// This will log a warning when debug logging is enabled but it is harmless.
EXPECT_FALSE(cache().Emplace(request));
}
TEST_F(PrefetchCacheTest, EmplaceTransientNIK) {
// This will log a warning when debug logging is enabled but it is harmless.
EXPECT_FALSE(cache().Emplace(MakeResourceRequest(
TestURL(), net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt))));
}
TEST_F(PrefetchCacheTest, EmplaceFileURL) {
// This will log a warning when debug logging is enabled but it is harmless.
EXPECT_FALSE(cache().Emplace(
MakeResourceRequest(GURL("file:///tmp/bogus.js"), TestIsolationInfo())));
}
TEST_F(PrefetchCacheTest, EmplaceSameURLNIK) {
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo())));
EXPECT_FALSE(
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo())));
}
TEST_F(PrefetchCacheTest, EmplaceDifferentURLSameNIK) {
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo())));
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));
}
TEST_F(PrefetchCacheTest, EmplaceSameURLDifferentNIK) {
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo(0))));
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo(1))));
}
TEST_F(PrefetchCacheTest, SuccessfulLookup) {
PrefetchURLLoaderClient* emplaced_client =
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo()));
EXPECT_TRUE(emplaced_client);
PrefetchURLLoaderClient* retrieved_client =
cache().Lookup(TestNIK(), TestURL());
ASSERT_TRUE(retrieved_client);
EXPECT_EQ(emplaced_client, retrieved_client);
EXPECT_EQ(retrieved_client->url(), TestURL());
EXPECT_EQ(retrieved_client->network_isolation_key(), TestNIK());
}
TEST_F(PrefetchCacheTest, FailedLookup) {
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo(1))));
EXPECT_FALSE(cache().Lookup(TestNIK(2), TestURL(2)));
}
TEST_F(PrefetchCacheTest, EmplaceRespectsMaxSize) {
// Insert kMaxSize distinct items into the cache.
for (size_t i = 0; i < kMaxSize; ++i) {
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(i), TestIsolationInfo())));
}
// Verify they are all still there.
for (size_t i = 0; i < kMaxSize; ++i) {
EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(i)));
}
// Add another item.
EXPECT_TRUE(cache().Emplace(
MakeResourceRequest(TestURL(kMaxSize), TestIsolationInfo())));
// The oldest one should now be gone.
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
// The rest should still be there.
for (size_t i = 1; i < kMaxSize + 1; ++i) {
EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(i)));
}
}
TEST_F(PrefetchCacheTest, ConsumedNodesNoLongerInCache) {
auto* client =
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo()));
cache().Consume(client);
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL()));
cache().Erase(client);
}
TEST_F(PrefetchCacheTest, ConsumedNodesDontCountTowardsCacheSize) {
// Insert kMaxSize distinct items into the cache.
for (size_t i = 0; i < kMaxSize; ++i) {
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(i), TestIsolationInfo())));
}
// Consume one of them.
auto* client = cache().Lookup(TestNIK(), TestURL(kMaxSize - 1));
cache().Consume(client);
// Insert a new item into the cache.
EXPECT_TRUE(cache().Emplace(
MakeResourceRequest(TestURL(kMaxSize), TestIsolationInfo())));
// Check the remaining items are all still there.
for (size_t i = 0; i < kMaxSize - 1; ++i) {
EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(i)));
}
cache().Erase(client);
}
TEST_F(PrefetchCacheTest, ExpiryHappens) {
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(), TestIsolationInfo())));
FastForwardBy(PrefetchCache::kMaxAge);
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL()));
}
// Prefetches that are submitted close together are expired from the cache
// together, to reduce the number of unnecessary wake-ups.
TEST_F(PrefetchCacheTest, TimerSlackIsApplied) {
const auto kTimeBetweenPrefetches = base::Milliseconds(1);
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo())));
FastForwardBy(kTimeBetweenPrefetches);
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));
FastForwardBy(PrefetchCache::kMaxAge - kTimeBetweenPrefetches);
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(1)));
}
TEST_F(PrefetchCacheTest, SeparatedPrefetchesExpiredSeparatedly) {
const auto kPrefetchSeparation = PrefetchCache::kMaxAge / 2;
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo())));
FastForwardBy(kPrefetchSeparation);
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));
FastForwardBy(PrefetchCache::kMaxAge - kPrefetchSeparation);
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(1)));
FastForwardBy(kPrefetchSeparation);
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(1)));
}
TEST_F(PrefetchCacheTest, ConsumedPrefetchesAreNotExpired) {
auto* client =
cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo()));
ASSERT_TRUE(client);
FastForwardBy(PrefetchCache::kMaxAge / 2);
EXPECT_TRUE(
cache().Emplace(MakeResourceRequest(TestURL(1), TestIsolationInfo())));
cache().Consume(client);
FastForwardBy(PrefetchCache::kMaxAge);
// An ASAN build would catch the error here if the client had been deleted.
EXPECT_EQ(client->url(), TestURL(0));
// The other entry still gets expired correctly.
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(1)));
cache().Erase(client);
}
TEST_F(PrefetchCacheTest, DelayedEraseErasesAfterADelay) {
auto* client =
cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo()));
ASSERT_TRUE(client);
cache().DelayedErase(client);
// Should be still there.
FastForwardBy(half_grace_time());
// Still there.
EXPECT_TRUE(cache().Lookup(TestNIK(), TestURL(0)));
FastForwardBy(erase_grace_time());
// Gone.
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
}
TEST_F(PrefetchCacheTest, MultipleDelayedErases) {
// This test requires that erase_grace_time() be evenly divisible by two.
ASSERT_EQ(half_grace_time() + half_grace_time(), erase_grace_time())
<< "This test requires that kEraseGraceTime be evenly divisible by two";
const auto still_there = [&](int client_index) {
return cache().Lookup(TestNIK(), TestURL(client_index));
};
for (int i = 0; i < 3; ++i) {
auto* client =
cache().Emplace(MakeResourceRequest(TestURL(i), TestIsolationInfo()));
ASSERT_TRUE(client);
cache().DelayedErase(client);
FastForwardBy(half_grace_time());
EXPECT_TRUE(still_there(i));
if (i >= 1) {
EXPECT_FALSE(still_there(i - 1));
}
}
EXPECT_TRUE(still_there(2));
FastForwardBy(half_grace_time());
EXPECT_FALSE(still_there(2));
}
TEST_F(PrefetchCacheTest, DelayedEraseRacesWithExpiry) {
auto* client =
cache().Emplace(MakeResourceRequest(TestURL(0), TestIsolationInfo()));
ASSERT_TRUE(client);
FastForwardBy(PrefetchCache::kMaxAge - half_grace_time());
cache().DelayedErase(client);
FastForwardBy(erase_grace_time());
// Should be gone now.
EXPECT_FALSE(cache().Lookup(TestNIK(), TestURL(0)));
}
} // namespace
} // namespace network