blob: cc2a14cc4f70a2409a9dd3d8b66b4126141ffc38 [file]
// Copyright 2026 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/disk_cache/sql/sql_shared_cache_manager.h"
#include <memory>
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "net/base/features.h"
#include "net/base/network_isolation_key.h"
#include "net/base/schemeful_site.h"
#include "net/disk_cache/backend_cleanup_tracker.h"
#include "net/disk_cache/sql/mock_shared_cache_client_remote.h"
#include "net/disk_cache/sql/sql_persistent_store.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace disk_cache {
class SqlSharedCacheManagerTest : public testing::TestWithParam<bool> {
public:
static std::string DescribeParams(
const testing::TestParamInfo<ParamType>& info) {
return info.param ? "WalEnabled" : "WalDisabled";
}
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
cleanup_tracker_ = BackendCleanupTracker::TryCreate(temp_dir_.GetPath(),
base::DoNothing());
CHECK(cleanup_tracker_);
if (GetParam()) {
feature_list_.InitWithFeaturesAndParameters(
{{net::features::kRendererAccessibleHttpCache,
{{net::features::kRendererAccessibleHttpCacheWalMode.name,
"true"}}}},
{});
} else {
feature_list_.InitWithFeaturesAndParameters(
{{net::features::kRendererAccessibleHttpCache,
{{net::features::kRendererAccessibleHttpCacheWalMode.name,
"false"}}}},
{});
}
task_runners_.push_back(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_BLOCKING,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN}));
CreateAndInitStore();
}
void TearDown() override {
store_.reset();
FlushPendingTask();
WaitForCleanup();
}
protected:
void WaitForCleanup() {
if (!cleanup_tracker_) {
return;
}
base::RunLoop run_loop;
cleanup_tracker_->AddPostCleanupCallback(run_loop.QuitClosure());
cleanup_tracker_ = nullptr;
run_loop.Run();
cleanup_tracker_ = BackendCleanupTracker::TryCreate(temp_dir_.GetPath(),
base::DoNothing());
CHECK(cleanup_tracker_);
}
void CreateStore(int64_t max_bytes = 0) {
CHECK(!store_);
store_ = std::make_unique<SqlPersistentStore>(
temp_dir_.GetPath(), max_bytes, net::CacheType::DISK_CACHE,
task_runners_, async_task_manager_, cleanup_tracker_);
}
SqlPersistentStore::Error Init() {
base::test::TestFuture<SqlPersistentStore::Error> future;
store_->Initialize(future.GetCallback());
return future.Get();
}
void CreateAndInitStore() {
CreateStore();
ASSERT_EQ(Init(), SqlPersistentStore::Error::kOk);
}
SqlSharedCacheManager* GetManager() {
return store_->shared_cache_manager_for_testing();
}
net::NetworkIsolationKey CreateNik(const std::string& url_str) {
net::SchemefulSite site((GURL(url_str)));
return net::NetworkIsolationKey(site, site);
}
void GetCacheByNik(
const net::NetworkIsolationKey& nik,
bool require_shared_cache_db_id,
base::OnceCallback<void(scoped_refptr<SqlSharedCacheHandle>)> callback) {
GetManager()->GetCacheByNik(nik, require_shared_cache_db_id,
std::move(callback));
}
void FlushPendingTask() {
async_task_manager_.RunUntilAllTasksCompleteForTest();
}
base::test::ScopedFeatureList feature_list_;
base::test::TaskEnvironment task_environment_;
base::ScopedTempDir temp_dir_;
std::vector<scoped_refptr<base::SequencedTaskRunner>> task_runners_;
SqlAsyncTaskManager async_task_manager_;
std::unique_ptr<SqlPersistentStore> store_;
scoped_refptr<BackendCleanupTracker> cleanup_tracker_;
};
INSTANTIATE_TEST_SUITE_P(All,
SqlSharedCacheManagerTest,
testing::Bool(),
&SqlSharedCacheManagerTest::DescribeParams);
TEST_P(SqlSharedCacheManagerTest, GetCacheByNikWithoutDbId) {
net::NetworkIsolationKey nik(net::SchemefulSite(GURL("https://foo.test")),
net::SchemefulSite(GURL("https://bar.test")));
scoped_refptr<SqlSharedCacheHandle> handle;
bool callback_run = false;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/false,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle = std::move(h);
callback_run = true;
}));
FlushPendingTask();
EXPECT_TRUE(callback_run);
ASSERT_TRUE(handle);
EXPECT_TRUE(handle->get() != nullptr);
EXPECT_FALSE((*handle)->shared_cache_db_id().has_value());
// Getting again with same NIK should return handle to same cache instance.
scoped_refptr<SqlSharedCacheHandle> handle2;
bool callback2_run = false;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/false,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle2 = std::move(h);
callback2_run = true;
}));
FlushPendingTask();
EXPECT_TRUE(callback2_run);
ASSERT_TRUE(handle2);
EXPECT_EQ(handle->get(), handle2->get());
}
TEST_P(SqlSharedCacheManagerTest, GetCacheByNikWithDbId) {
net::NetworkIsolationKey nik(net::SchemefulSite(GURL("https://foo.test")),
net::SchemefulSite(GURL("https://bar.test")));
scoped_refptr<SqlSharedCacheHandle> handle;
bool callback_run = false;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/true,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle = std::move(h);
callback_run = true;
}));
FlushPendingTask();
EXPECT_TRUE(callback_run);
ASSERT_TRUE(handle);
EXPECT_TRUE(handle->get() != nullptr);
EXPECT_TRUE((*handle)->shared_cache_db_id().has_value());
SqlSharedCacheDbId db_id = *(*handle)->shared_cache_db_id();
// Now query by DbId
scoped_refptr<SqlSharedCacheHandle> handle_by_id;
bool callback_by_id_run = false;
GetManager()->GetCacheByDbId(
db_id,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle_by_id = std::move(h);
callback_by_id_run = true;
}));
FlushPendingTask();
EXPECT_TRUE(callback_by_id_run);
ASSERT_TRUE(handle_by_id);
EXPECT_EQ(handle->get(), handle_by_id->get());
}
TEST_P(SqlSharedCacheManagerTest, GetCacheByNikUpgradeToDbId) {
net::NetworkIsolationKey nik(net::SchemefulSite(GURL("https://foo.test")),
net::SchemefulSite(GURL("https://bar.test")));
// First create without DbId requirement
scoped_refptr<SqlSharedCacheHandle> handle;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/false,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle = std::move(h);
}));
FlushPendingTask();
ASSERT_TRUE(handle);
EXPECT_FALSE((*handle)->shared_cache_db_id().has_value());
// Request again with require_shared_cache_db_id = true
scoped_refptr<SqlSharedCacheHandle> handle_with_id;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/true,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle_with_id = std::move(h);
}));
FlushPendingTask();
ASSERT_TRUE(handle_with_id);
EXPECT_EQ(handle->get(), handle_with_id->get());
EXPECT_TRUE((*handle_with_id)->shared_cache_db_id().has_value());
}
TEST_P(SqlSharedCacheManagerTest, GetCacheByDbIdNonExistent) {
scoped_refptr<SqlSharedCacheHandle> handle;
bool callback_run = false;
GetManager()->GetCacheByDbId(
SqlSharedCacheDbId(99999),
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle = std::move(h);
callback_run = true;
}));
FlushPendingTask();
EXPECT_TRUE(callback_run);
EXPECT_FALSE(handle);
}
TEST_P(SqlSharedCacheManagerTest, CacheUnreferencedDeletion) {
net::NetworkIsolationKey nik(net::SchemefulSite(GURL("https://foo.test")),
net::SchemefulSite(GURL("https://bar.test")));
{
scoped_refptr<SqlSharedCacheHandle> handle;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/false,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle = std::move(h);
}));
FlushPendingTask();
ASSERT_TRUE(handle);
}
// Handle went out of scope, unreferenced task should post and run.
FlushPendingTask();
// Fetching again should create a new cache instance.
scoped_refptr<SqlSharedCacheHandle> new_handle;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/false,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
new_handle = std::move(h);
}));
FlushPendingTask();
ASSERT_TRUE(new_handle);
}
TEST_P(SqlSharedCacheManagerTest, DestructionTriggersCleanup) {
net::NetworkIsolationKey nik(net::SchemefulSite(GURL("https://foo.test")),
net::SchemefulSite(GURL("https://bar.test")));
scoped_refptr<SqlSharedCacheHandle> handle;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/true,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle = std::move(h);
}));
FlushPendingTask();
ASSERT_TRUE(handle);
handle.reset();
FlushPendingTask();
// Destroying store resets `shared_cache_manager_` which triggers Close in
// destructor.
store_.reset();
// `RunUntilAllTasksCompleteForTest()` waits for the async close task to
// complete.
FlushPendingTask();
WaitForCleanup();
// Re-creating and initializing a new store (and thus SqlSharedCacheManager)
// should succeed.
CreateAndInitStore();
}
TEST_P(SqlSharedCacheManagerTest, DeleteResourcesEmpty) {
base::test::TestFuture<void> future;
GetManager()->DeleteResources({}, future.GetCallback());
FlushPendingTask();
EXPECT_TRUE(future.IsReady());
}
TEST_P(SqlSharedCacheManagerTest, DeleteResourcesNonExistentDbId) {
base::test::TestFuture<void> future;
GetManager()->DeleteResources(
{{SqlSharedCacheDbId(99999), SqlSharedCacheRowId(1)}},
future.GetCallback());
FlushPendingTask();
EXPECT_TRUE(future.IsReady());
}
TEST_P(SqlSharedCacheManagerTest, DeleteResourcesSingleCache) {
net::NetworkIsolationKey nik(net::SchemefulSite(GURL("https://foo.test")),
net::SchemefulSite(GURL("https://bar.test")));
scoped_refptr<SqlSharedCacheHandle> handle;
GetManager()->GetCacheByNik(
nik, /*require_shared_cache_db_id=*/true,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle = std::move(h);
}));
FlushPendingTask();
ASSERT_TRUE(handle);
ASSERT_TRUE((*handle)->shared_cache_db_id().has_value());
SqlSharedCacheDbId db_id = *(*handle)->shared_cache_db_id();
// Insert two test entries.
CacheEntryKey key1("0/0/https://example.com/1");
CacheEntryKey key2("0/0/https://example.com/2");
auto headers = base::MakeRefCounted<net::IOBufferWithSize>(4);
headers->span().copy_from(base::span<const uint8_t>({1, 2, 3, 4}));
auto body = base::MakeRefCounted<net::IOBufferWithSize>(3);
body->span().copy_from(base::span<const uint8_t>({5, 6, 7}));
base::test::TestFuture<base::expected<SqlSharedCacheRowId,
SqlSharedCacheIsolatedDatabase::Error>>
insert_future1;
(*handle)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert)
.WithArgs(key1, headers, 3, body)
.Then(insert_future1.GetCallback());
base::test::TestFuture<base::expected<SqlSharedCacheRowId,
SqlSharedCacheIsolatedDatabase::Error>>
insert_future2;
(*handle)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert)
.WithArgs(key2, headers, 3, body)
.Then(insert_future2.GetCallback());
FlushPendingTask();
auto insert_res1 = insert_future1.Take();
auto insert_res2 = insert_future2.Take();
ASSERT_TRUE(insert_res1.has_value());
ASSERT_TRUE(insert_res2.has_value());
SqlSharedCacheRowId row1 = *insert_res1;
SqlSharedCacheRowId row2 = *insert_res2;
// Verify entries can be read before deletion.
auto read_buf = base::MakeRefCounted<net::IOBufferWithSize>(3);
base::test::TestFuture<SqlSharedCacheIsolatedDatabase::ReadResultOrError>
read_before_future;
(*handle)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Read)
.WithArgs(key1, row1, /*body_size=*/3, /*offset=*/0, read_buf)
.Then(read_before_future.GetCallback());
FlushPendingTask();
EXPECT_TRUE(read_before_future.Get().has_value());
// Delete resources using SqlSharedCacheManager.
base::test::TestFuture<void> delete_future;
GetManager()->DeleteResources({{db_id, row1}, {db_id, row2}},
delete_future.GetCallback());
FlushPendingTask();
EXPECT_TRUE(delete_future.IsReady());
// Verify entries cannot be read after deletion.
base::test::TestFuture<SqlSharedCacheIsolatedDatabase::ReadResultOrError>
read_after_future1;
(*handle)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Read)
.WithArgs(key1, row1, /*body_size=*/3, /*offset=*/0, read_buf)
.Then(read_after_future1.GetCallback());
base::test::TestFuture<SqlSharedCacheIsolatedDatabase::ReadResultOrError>
read_after_future2;
(*handle)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Read)
.WithArgs(key2, row2, /*body_size=*/3, /*offset=*/0, read_buf)
.Then(read_after_future2.GetCallback());
FlushPendingTask();
auto read_res1 = read_after_future1.Take();
auto read_res2 = read_after_future2.Take();
EXPECT_FALSE(read_res1.has_value());
EXPECT_EQ(read_res1.error(),
SqlSharedCacheIsolatedDatabase::Error::kEntryNotFound);
EXPECT_FALSE(read_res2.has_value());
EXPECT_EQ(read_res2.error(),
SqlSharedCacheIsolatedDatabase::Error::kEntryNotFound);
}
TEST_P(SqlSharedCacheManagerTest, DeleteResourcesMultipleCaches) {
net::NetworkIsolationKey nik1(net::SchemefulSite(GURL("https://foo1.test")),
net::SchemefulSite(GURL("https://bar1.test")));
net::NetworkIsolationKey nik2(net::SchemefulSite(GURL("https://foo2.test")),
net::SchemefulSite(GURL("https://bar2.test")));
scoped_refptr<SqlSharedCacheHandle> handle1;
GetManager()->GetCacheByNik(
nik1, /*require_shared_cache_db_id=*/true,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle1 = std::move(h);
}));
scoped_refptr<SqlSharedCacheHandle> handle2;
GetManager()->GetCacheByNik(
nik2, /*require_shared_cache_db_id=*/true,
base::BindLambdaForTesting([&](scoped_refptr<SqlSharedCacheHandle> h) {
handle2 = std::move(h);
}));
FlushPendingTask();
ASSERT_TRUE(handle1);
ASSERT_TRUE(handle2);
ASSERT_TRUE((*handle1)->shared_cache_db_id().has_value());
ASSERT_TRUE((*handle2)->shared_cache_db_id().has_value());
SqlSharedCacheDbId db_id1 = *(*handle1)->shared_cache_db_id();
SqlSharedCacheDbId db_id2 = *(*handle2)->shared_cache_db_id();
// Insert entries into cache 1 and cache 2.
CacheEntryKey key1("0/0/https://example.com/1");
CacheEntryKey key2("0/0/https://example.com/2");
auto headers = base::MakeRefCounted<net::IOBufferWithSize>(4);
headers->span().copy_from(base::span<const uint8_t>({1, 2, 3, 4}));
auto body = base::MakeRefCounted<net::IOBufferWithSize>(3);
body->span().copy_from(base::span<const uint8_t>({5, 6, 7}));
base::test::TestFuture<base::expected<SqlSharedCacheRowId,
SqlSharedCacheIsolatedDatabase::Error>>
insert_future1;
(*handle1)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert)
.WithArgs(key1, headers, 3, body)
.Then(insert_future1.GetCallback());
base::test::TestFuture<base::expected<SqlSharedCacheRowId,
SqlSharedCacheIsolatedDatabase::Error>>
insert_future2;
(*handle2)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert)
.WithArgs(key2, headers, 3, body)
.Then(insert_future2.GetCallback());
FlushPendingTask();
auto insert_res1 = insert_future1.Take();
auto insert_res2 = insert_future2.Take();
ASSERT_TRUE(insert_res1.has_value());
ASSERT_TRUE(insert_res2.has_value());
SqlSharedCacheRowId row1 = *insert_res1;
SqlSharedCacheRowId row2 = *insert_res2;
// Delete resources across multiple caches.
base::test::TestFuture<void> delete_future;
GetManager()->DeleteResources({{db_id1, row1}, {db_id2, row2}},
delete_future.GetCallback());
FlushPendingTask();
EXPECT_TRUE(delete_future.IsReady());
// Verify entries in both caches cannot be read after deletion.
auto read_buf = base::MakeRefCounted<net::IOBufferWithSize>(3);
base::test::TestFuture<SqlSharedCacheIsolatedDatabase::ReadResultOrError>
read_after_future1;
(*handle1)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Read)
.WithArgs(key1, row1, /*body_size=*/3, /*offset=*/0, read_buf)
.Then(read_after_future1.GetCallback());
base::test::TestFuture<SqlSharedCacheIsolatedDatabase::ReadResultOrError>
read_after_future2;
(*handle2)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Read)
.WithArgs(key2, row2, /*body_size=*/3, /*offset=*/0, read_buf)
.Then(read_after_future2.GetCallback());
FlushPendingTask();
auto read_res1 = read_after_future1.Take();
auto read_res2 = read_after_future2.Take();
EXPECT_FALSE(read_res1.has_value());
EXPECT_EQ(read_res1.error(),
SqlSharedCacheIsolatedDatabase::Error::kEntryNotFound);
EXPECT_FALSE(read_res2.has_value());
EXPECT_EQ(read_res2.error(),
SqlSharedCacheIsolatedDatabase::Error::kEntryNotFound);
}
TEST_P(SqlSharedCacheManagerTest, RegisterClientNewCache) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
auto client = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr = client.get();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
manager->RegisterClient(nik, std::move(client));
// Wait for the cache to be registered in the manager, which involves
// async DB access. The disconnect handler is set during cache creation.
client_ptr->WaitUntilDisconnectHandlerSet();
// The cache is created but client is not initialized because there is no DB
// ID yet.
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u);
EXPECT_FALSE(client_ptr->initialize_called());
}
TEST_P(SqlSharedCacheManagerTest, RegisterClientExistingCacheNoDbId) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
auto client1 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr1 = client1.get();
auto client2 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr2 = client2.get();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
// Register the first client.
manager->RegisterClient(nik, std::move(client1));
// Register the second client immediately before db_task_runner finishes.
manager->RegisterClient(nik, std::move(client2));
client_ptr1->WaitUntilDisconnectHandlerSet();
client_ptr2->WaitUntilDisconnectHandlerSet();
// Both clients registered, but neither is initialized.
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u);
EXPECT_FALSE(client_ptr1->initialize_called());
EXPECT_FALSE(client_ptr2->initialize_called());
// Requesting the cache with require_shared_cache_db_id=true allocates a DB ID
// and initializes the underlying isolated database.
base::test::TestFuture<scoped_refptr<SqlSharedCacheHandle>> future;
GetCacheByNik(nik, /*require_shared_cache_db_id=*/true, future.GetCallback());
scoped_refptr<SqlSharedCacheHandle> keep_alive_handle = future.Take();
ASSERT_TRUE(keep_alive_handle);
// Both clients should now be initialized.
client_ptr1->WaitUntilInitialized();
client_ptr2->WaitUntilInitialized();
EXPECT_EQ(client_ptr1->initialize_call_count(), 1u);
EXPECT_EQ(client_ptr2->initialize_call_count(), 1u);
}
TEST_P(SqlSharedCacheManagerTest,
RegisterClientWhileInitIsolatedDatabaseInFlight) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
auto client1 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr1 = client1.get();
auto client2 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr2 = client2.get();
auto client3 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr3 = client3.get();
// 1. Register client1 before InitIsolatedDatabase.
manager->RegisterClient(nik, std::move(client1));
client_ptr1->WaitUntilDisconnectHandlerSet();
EXPECT_EQ(client_ptr1->initialize_call_count(), 0u);
// 2. Request cache with require_shared_cache_db_id=true to trigger
// InitIsolatedDatabase asynchronously.
base::test::TestFuture<scoped_refptr<SqlSharedCacheHandle>> future;
GetCacheByNik(nik, /*require_shared_cache_db_id=*/true, future.GetCallback());
// 3. Immediately register client2 while InitIsolatedDatabase is in flight.
manager->RegisterClient(nik, std::move(client2));
scoped_refptr<SqlSharedCacheHandle> keep_alive_handle = future.Take();
ASSERT_TRUE(keep_alive_handle);
// 4. Register client3 after DB initialization is complete.
manager->RegisterClient(nik, std::move(client3));
// Wait for all clients to be initialized.
client_ptr1->WaitUntilInitialized();
client_ptr2->WaitUntilInitialized();
client_ptr3->WaitUntilInitialized();
FlushPendingTask();
// Verify that each client is initialized exactly once.
EXPECT_EQ(client_ptr1->initialize_call_count(), 1u);
EXPECT_EQ(client_ptr2->initialize_call_count(), 1u);
EXPECT_EQ(client_ptr3->initialize_call_count(), 1u);
}
TEST_P(SqlSharedCacheManagerTest, RegisterClientExistingCacheWithDbId) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
auto client1 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr1 = client1.get();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
// Force creation of a DB ID by requesting with
// require_shared_cache_db_id=true.
base::test::TestFuture<scoped_refptr<SqlSharedCacheHandle>> future;
GetCacheByNik(nik, /*require_shared_cache_db_id=*/true, future.GetCallback());
scoped_refptr<SqlSharedCacheHandle> keep_alive_handle = future.Get();
EXPECT_TRUE(keep_alive_handle);
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u);
EXPECT_EQ(manager->GetSharedCachesByDbIdSizeForTest(), 1u);
// Register the first client. The cache should already have a db_id.
manager->RegisterClient(nik, std::move(client1));
// Now it should be initialized because the DB ID exists.
client_ptr1->WaitUntilInitialized();
EXPECT_TRUE(client_ptr1->initialize_called());
}
TEST_P(SqlSharedCacheManagerTest, RegisterClientDifferentNik) {
auto* manager = GetManager();
net::NetworkIsolationKey nik1 = CreateNik("https://example.com");
net::NetworkIsolationKey nik2 = CreateNik("https://example.net");
auto client1 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr1 = client1.get();
auto client2 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr2 = client2.get();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
manager->RegisterClient(nik1, std::move(client1));
manager->RegisterClient(nik2, std::move(client2));
client_ptr1->WaitUntilDisconnectHandlerSet();
client_ptr2->WaitUntilDisconnectHandlerSet();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 2u);
EXPECT_FALSE(client_ptr1->initialize_called());
EXPECT_FALSE(client_ptr2->initialize_called());
}
TEST_P(SqlSharedCacheManagerTest, ClientDisconnected) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
auto client = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr = client.get();
manager->RegisterClient(nik, std::move(client));
client_ptr->WaitUntilDisconnectHandlerSet();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u);
EXPECT_TRUE(client_ptr->has_disconnect_handler());
// Simulate client disconnection.
client_ptr->RunDisconnectHandler();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return manager->GetSharedCachesSizeForTest() == 0u; }));
}
TEST_P(SqlSharedCacheManagerTest, ClientDisconnectBeforeDbInit) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
auto client = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr = client.get();
manager->RegisterClient(nik, std::move(client));
client_ptr->WaitUntilDisconnectHandlerSet();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u);
// Disconnect before DB initialization.
client_ptr->RunDisconnectHandler();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return manager->GetSharedCachesSizeForTest() == 0u; }));
FlushPendingTask();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
}
TEST_P(SqlSharedCacheManagerTest, PartialClientDisconnect) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
auto client1 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr1 = client1.get();
auto client2 = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr2 = client2.get();
manager->RegisterClient(nik, std::move(client1));
manager->RegisterClient(nik, std::move(client2));
client_ptr1->WaitUntilDisconnectHandlerSet();
client_ptr2->WaitUntilDisconnectHandlerSet();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u);
// Disconnect the first client. The cache should remain alive because client2
// is still connected.
client_ptr1->RunDisconnectHandler();
FlushPendingTask();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 1u);
// Disconnect the second client. Now the cache should be destroyed.
client_ptr2->RunDisconnectHandler();
ASSERT_TRUE(base::test::RunUntil(
[&]() { return manager->GetSharedCachesSizeForTest() == 0u; }));
}
TEST_P(SqlSharedCacheManagerTest, RegisterClientDbFailure) {
auto* manager = GetManager();
manager->SetSimulateDbFailureForTesting(true);
net::NetworkIsolationKey nik = CreateNik("https://example.com");
base::RunLoop destroy_run_loop;
auto client = std::make_unique<MockSharedCacheClientRemote>();
client->SetOnDestroyHandler(destroy_run_loop.QuitClosure());
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
manager->RegisterClient(nik, std::move(client));
// The client should be destroyed because DB initialization fails and
// the manager drops the client.
destroy_run_loop.Run();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
}
TEST_P(SqlSharedCacheManagerTest,
RegisterClientExistingCacheWithDbIdAndHashes) {
auto* manager = GetManager();
net::NetworkIsolationKey nik = CreateNik("https://example.com");
// Force creation of a DB ID.
base::test::TestFuture<scoped_refptr<SqlSharedCacheHandle>> future;
GetCacheByNik(nik, /*require_shared_cache_db_id=*/true, future.GetCallback());
scoped_refptr<SqlSharedCacheHandle> keep_alive_handle = future.Take();
ASSERT_TRUE(keep_alive_handle);
// Insert an entry into the isolated database.
const std::string kKey1 = "0/0/https://example.com/res1";
const std::string kKey2 = "0/0/https://example.com/res2";
CacheEntryKey entry_key1(kKey1);
CacheEntryKey entry_key2(kKey2);
const std::string kData = "Data";
auto buffer = base::MakeRefCounted<net::StringIOBuffer>(kData);
base::test::TestFuture<base::expected<SqlSharedCacheRowId,
SqlSharedCacheIsolatedDatabase::Error>>
insert_future1;
(*keep_alive_handle)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert)
.WithArgs(entry_key1, /*headers=*/nullptr, kData.size(), buffer)
.Then(insert_future1.GetCallback());
FlushPendingTask();
ASSERT_TRUE(insert_future1.Take().has_value());
base::test::TestFuture<base::expected<SqlSharedCacheRowId,
SqlSharedCacheIsolatedDatabase::Error>>
insert_future2;
(*keep_alive_handle)
->isolated_database_for_testing()
.AsyncCall(&SqlSharedCacheIsolatedDatabase::Insert)
.WithArgs(entry_key2, /*headers=*/nullptr, kData.size(), buffer)
.Then(insert_future2.GetCallback());
FlushPendingTask();
ASSERT_TRUE(insert_future2.Take().has_value());
// Release the keep-alive handle so the cache instance is destroyed.
keep_alive_handle = nullptr;
FlushPendingTask();
EXPECT_EQ(manager->GetSharedCachesSizeForTest(), 0u);
// Register client for the existing NIK with stored resources.
// The newly created cache should load existing hashes from the database.
auto client = std::make_unique<MockSharedCacheClientRemote>();
auto* client_ptr = client.get();
manager->RegisterClient(nik, std::move(client));
client_ptr->WaitUntilInitialized();
client_ptr->WaitUntilOnResourcesAdded();
EXPECT_TRUE(client_ptr->initialize_called());
EXPECT_TRUE(client_ptr->on_resources_added_called());
EXPECT_EQ(client_ptr->new_hashes().size(), 2u);
std::vector<uint32_t> expected_hashes = {
static_cast<uint32_t>(entry_key1.resource_url_hash().value()),
static_cast<uint32_t>(entry_key2.resource_url_hash().value())};
EXPECT_THAT(client_ptr->new_hashes(),
testing::UnorderedElementsAreArray(expected_hashes));
}
} // namespace disk_cache