blob: 7538ff6c66aa2cb9a825bf2515821f67ec09df46 [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/indexed_db/indexed_db_factory.h"
#include <stdint.h>
#include <ctime>
#include <memory>
#include <utility>
#include "base/auto_reset.h"
#include "base/barrier_closure.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/test/test_simple_task_runner.h"
#include "components/services/storage/indexed_db/leveldb/fake_leveldb_factory.h"
#include "components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_database.h"
#include "components/services/storage/privileged/mojom/indexed_db_control.mojom-test-utils.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "components/services/storage/public/mojom/storage_usage_info.mojom.h"
#include "content/browser/indexed_db/indexed_db_backing_store.h"
#include "content/browser/indexed_db/indexed_db_bucket_context.h"
#include "content/browser/indexed_db/indexed_db_class_factory.h"
#include "content/browser/indexed_db/indexed_db_connection.h"
#include "content/browser/indexed_db/indexed_db_context_impl.h"
#include "content/browser/indexed_db/indexed_db_data_format_version.h"
#include "content/browser/indexed_db/indexed_db_pre_close_task_queue.h"
#include "content/browser/indexed_db/indexed_db_transaction.h"
#include "content/browser/indexed_db/mock_indexed_db_factory_client.h"
#include "content/browser/indexed_db/mock_mojo_indexed_db_database_callbacks.h"
#include "content/browser/indexed_db/mock_mojo_indexed_db_factory_client.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "net/base/features.h"
#include "storage/browser/test/mock_quota_manager_proxy.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
using blink::IndexedDBDatabaseMetadata;
using testing::_;
using url::Origin;
namespace content {
namespace {
ACTION_TEMPLATE(MoveArgPointee,
HAS_1_TEMPLATE_PARAMS(int, k),
AND_1_VALUE_PARAMS(out)) {
*out = std::move(*::testing::get<k>(args));
}
storage::BucketInfo ToBucketInfo(const storage::BucketLocator& bucket_locator) {
storage::BucketInfo bucket_info;
bucket_info.id = bucket_locator.id;
bucket_info.storage_key = bucket_locator.storage_key;
bucket_info.name = storage::kDefaultBucketName;
return bucket_info;
}
} // namespace
class IndexedDBFactoryTest : public testing::Test {
public:
IndexedDBFactoryTest() = default;
IndexedDBFactoryTest(const IndexedDBFactoryTest&) = delete;
IndexedDBFactoryTest& operator=(const IndexedDBFactoryTest&) = delete;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
quota_policy_ = base::MakeRefCounted<storage::MockSpecialStoragePolicy>();
quota_manager_ = base::MakeRefCounted<storage::MockQuotaManager>(
/*is_incognito=*/false, temp_dir_.GetPath(),
base::SingleThreadTaskRunner::GetCurrentDefault().get(),
quota_policy_.get());
quota_manager_proxy_ = base::MakeRefCounted<storage::MockQuotaManagerProxy>(
quota_manager_.get(),
base::SingleThreadTaskRunner::GetCurrentDefault().get());
}
void TearDown() override {
if (context_ && !context_->IsInMemoryContext()) {
IndexedDBFactory* factory = context_->GetIDBFactory();
// Loop through all open storage keys, and force close them, and request
// the deletion of the leveldb state. Once the states are no longer
// around, delete all of the databases on disk.
for (const auto& bucket_id : factory->GetOpenBucketIdsForTesting()) {
context_->ForceClose(
bucket_id,
storage::mojom::ForceCloseReason::FORCE_CLOSE_DELETE_ORIGIN,
base::DoNothing());
}
// All leveldb databases are closed, and they can be deleted.
for (auto bucket_locator : context_->GetAllBuckets()) {
base::test::TestFuture<bool> success;
context_->DeleteForStorageKey(bucket_locator.storage_key,
success.GetCallback());
EXPECT_TRUE(success.Get());
}
}
quota_manager_.reset();
// Wait for mojo pipes to flush or there may be leaks.
task_environment_.RunUntilIdle();
}
IndexedDBBucketContextHandle CreateBucketHandle(
absl::optional<storage::BucketLocator> bucket_locator = absl::nullopt) {
if (!bucket_locator) {
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
bucket_locator = storage::BucketLocator();
bucket_locator->storage_key = storage_key;
}
IndexedDBBucketContextHandle bucket_context_handle(
factory()->GetOrCreateBucketContext(
ToBucketInfo(*bucket_locator),
context()->GetDataPath(*bucket_locator)));
bucket_context_handle->InitBackingStoreIfNeeded(/*create_if_missing=*/true);
return bucket_context_handle;
}
void SetUpContext() {
context_ = std::make_unique<IndexedDBContextImpl>(
temp_dir_.GetPath(), quota_manager_proxy_.get(),
/*blob_storage_context=*/mojo::NullRemote(),
/*file_system_access_context=*/mojo::NullRemote(),
base::SequencedTaskRunner::GetCurrentDefault(),
base::SequencedTaskRunner::GetCurrentDefault());
}
void SetUpInMemoryContext() {
context_ = std::make_unique<IndexedDBContextImpl>(
base::FilePath(), quota_manager_proxy_.get(),
/*blob_storage_context=*/mojo::NullRemote(),
/*file_system_access_context=*/mojo::NullRemote(),
base::SequencedTaskRunner::GetCurrentDefault(),
base::SequencedTaskRunner::GetCurrentDefault());
}
void RunPostedTasks() {
base::RunLoop loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, loop.QuitClosure());
loop.Run();
}
storage::BucketInfo GetOrCreateBucket(
const storage::BucketInitParams& params) {
base::test::TestFuture<storage::QuotaErrorOr<storage::BucketInfo>> future;
quota_manager_proxy_->UpdateOrCreateBucket(
params, base::SingleThreadTaskRunner::GetCurrentDefault(),
future.GetCallback());
return future.Take().value();
}
void BindIndexedDBFactory(
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote,
mojo::PendingReceiver<blink::mojom::IDBFactory> receiver,
storage::QuotaErrorOr<storage::BucketInfo> bucket_info) {
context()->BindIndexedDBImpl(std::move(checker_remote),
base::UnguessableToken(), std::move(receiver),
bucket_info);
}
void VerifyBucketContext(
const storage::BucketId& id,
bool expected_context_exists,
std::optional<bool> expected_backing_store_exists = std::nullopt) {
IndexedDBBucketContext* context = factory()->GetBucketContextForTesting(id);
if (!expected_context_exists) {
EXPECT_FALSE(context);
EXPECT_FALSE(expected_backing_store_exists.has_value());
} else {
ASSERT_TRUE(context);
if (expected_backing_store_exists.has_value()) {
EXPECT_EQ(*expected_backing_store_exists, !!context->backing_store());
}
}
}
protected:
IndexedDBContextImpl* context() const { return context_.get(); }
IndexedDBFactory* factory() const { return context_->GetIDBFactory(); }
storage::MockQuotaManager* quota_manager() { return quota_manager_.get(); }
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
base::ScopedTempDir temp_dir_;
scoped_refptr<storage::MockSpecialStoragePolicy> quota_policy_;
scoped_refptr<storage::MockQuotaManager> quota_manager_;
scoped_refptr<storage::MockQuotaManagerProxy> quota_manager_proxy_;
std::unique_ptr<IndexedDBContextImpl> context_;
std::unique_ptr<MockIndexedDBFactoryClient> mock_factory_client_;
};
class IndexedDBFactoryTestWithStoragePartitioning
: public IndexedDBFactoryTest,
public testing::WithParamInterface<bool> {
public:
IndexedDBFactoryTestWithStoragePartitioning() {
feature_list_.InitWithFeatureState(
net::features::kThirdPartyStoragePartitioning,
IsThirdPartyStoragePartitioningEnabled());
}
bool IsThirdPartyStoragePartitioningEnabled() { return GetParam(); }
private:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
/* no prefix */,
IndexedDBFactoryTestWithStoragePartitioning,
testing::Bool());
TEST_P(IndexedDBFactoryTestWithStoragePartitioning,
BasicFactoryCreationAndTearDown) {
SetUpContext();
const blink::StorageKey storage_key_1 =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
storage::BucketInfo bucket_1 = GetOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key_1));
storage::BucketLocator bucket_locator_1 = bucket_1.ToBucketLocator();
auto file_1 = context_->GetLevelDBPathForTesting(bucket_locator_1)
.AppendASCII("1.json");
ASSERT_TRUE(CreateDirectory(file_1.DirName()));
ASSERT_TRUE(base::WriteFile(file_1, std::string(10, 'a')));
const blink::StorageKey storage_key_2 =
blink::StorageKey::CreateFromStringForTesting("http://localhost:82");
storage::BucketInfo bucket_2 = GetOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key_2));
storage::BucketLocator bucket_locator_2 = bucket_2.ToBucketLocator();
auto file_2 = context_->GetLevelDBPathForTesting(bucket_locator_2)
.AppendASCII("2.json");
ASSERT_TRUE(CreateDirectory(file_2.DirName()));
ASSERT_TRUE(base::WriteFile(file_2, std::string(100, 'a')));
const blink::StorageKey storage_key_3 =
blink::StorageKey::CreateFromStringForTesting("http://localhost2:82");
storage::BucketInfo bucket_3 = GetOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key_3));
storage::BucketLocator bucket_locator_3 = bucket_3.ToBucketLocator();
auto file_3 = context_->GetLevelDBPathForTesting(bucket_locator_3)
.AppendASCII("3.json");
ASSERT_TRUE(CreateDirectory(file_3.DirName()));
ASSERT_TRUE(base::WriteFile(file_3, std::string(1000, 'a')));
const blink::StorageKey storage_key_4 = blink::StorageKey::Create(
storage_key_1.origin(), net::SchemefulSite(storage_key_3.origin()),
blink::mojom::AncestorChainBit::kCrossSite);
storage::BucketInfo bucket_4 = GetOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(storage_key_4));
storage::BucketLocator bucket_locator_4 = bucket_4.ToBucketLocator();
auto file_4 = context_->GetLevelDBPathForTesting(bucket_locator_4)
.AppendASCII("4.json");
ASSERT_TRUE(CreateDirectory(file_4.DirName()));
ASSERT_TRUE(base::WriteFile(file_4, std::string(10000, 'a')));
const blink::StorageKey storage_key_5 = storage_key_1;
storage::BucketInitParams params(storage_key_5, "inbox");
storage::BucketInfo bucket_5 = GetOrCreateBucket(params);
storage::BucketLocator bucket_locator_5 = bucket_5.ToBucketLocator();
auto file_5 = context_->GetLevelDBPathForTesting(bucket_locator_5)
.AppendASCII("5.json");
ASSERT_TRUE(CreateDirectory(file_5.DirName()));
ASSERT_TRUE(base::WriteFile(file_5, std::string(20000, 'a')));
EXPECT_NE(file_5.DirName(), file_1.DirName());
factory()
->GetOrCreateBucketContext(bucket_1,
context()->GetDataPath(bucket_locator_1))
.InitBackingStoreIfNeeded(true);
factory()
->GetOrCreateBucketContext(bucket_2,
context()->GetDataPath(bucket_locator_2))
.InitBackingStoreIfNeeded(true);
factory()
->GetOrCreateBucketContext(bucket_3,
context()->GetDataPath(bucket_locator_3))
.InitBackingStoreIfNeeded(true);
factory()
->GetOrCreateBucketContext(bucket_4,
context()->GetDataPath(bucket_locator_4))
.InitBackingStoreIfNeeded(true);
factory()
->GetOrCreateBucketContext(bucket_5,
context()->GetDataPath(bucket_locator_5))
.InitBackingStoreIfNeeded(true);
int64_t bucket_size_1 = base::ComputeDirectorySize(file_1.DirName());
int64_t bucket_size_4 = base::ComputeDirectorySize(file_4.DirName());
int64_t bucket_size_5 = base::ComputeDirectorySize(file_5.DirName());
if (IsThirdPartyStoragePartitioningEnabled()) {
// If third party storage partitioning is on, additional space is taken
// by supporting files for the independent buckets.
EXPECT_NE(bucket_size_1, bucket_size_4);
}
EXPECT_NE(bucket_size_1, bucket_size_5);
if (IsThirdPartyStoragePartitioningEnabled()) {
EXPECT_EQ(5ul, factory()->GetOpenBucketIdsForTesting().size());
} else {
EXPECT_EQ(4ul, factory()->GetOpenBucketIdsForTesting().size());
}
}
TEST_F(IndexedDBFactoryTest, CloseSequenceStarts) {
SetUpContext();
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
const storage::BucketId bucket_id =
bucket_context_handle->bucket_locator().id;
bucket_context_handle.Release();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
EXPECT_TRUE(factory()->GetBucketContextForTesting(bucket_id)->IsClosing());
factory()->ForceClose(bucket_id, false);
RunPostedTasks();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/false);
}
TEST_F(IndexedDBFactoryTest, ImmediateClose) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
kIDBCloseImmediatelySwitch);
SetUpContext();
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
const storage::BucketId bucket_id =
bucket_context_handle->bucket_locator().id;
bucket_context_handle.Release();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true);
RunPostedTasks();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/false);
}
// Similar to the above, but installs a receiver which prevents the bucket
// context from being destroyed.
TEST_F(IndexedDBFactoryTest, CloseWithReceiversActive) {
SetUpContext();
// Create bucket context.
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
const storage::BucketId bucket_id =
bucket_context_handle->bucket_locator().id;
// Connect an IDBFactory mojo client.
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
bucket_context_handle->AddReceiver(
std::move(checker_remote), /*client_token=*/{},
factory_remote.BindNewPipeAndPassReceiver());
// The bucket context and the backing store should exist.
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
// The last handle to the bucket context is released and the grace period
// elapses.
bucket_context_handle.Release();
task_environment_.FastForwardBy(base::Seconds(2));
// This destroys the backing store, but the bucket context itself still
// exists...
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/false);
// ...until the last mojo client is disconnected.
factory_remote.reset();
task_environment_.RunUntilIdle();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/false);
}
// Similar to the above, but reverses the order of receiver disconnection and
// handle destruction.
TEST_F(IndexedDBFactoryTest, CloseWithReceiversInactive) {
SetUpContext();
// Create bucket context.
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
const storage::BucketId bucket_id =
bucket_context_handle->bucket_locator().id;
// Connect an IDBFactory mojo client.
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
bucket_context_handle->AddReceiver(
std::move(checker_remote), /*client_token=*/{},
factory_remote.BindNewPipeAndPassReceiver());
// The bucket context and the backing store should exist.
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
// The last mojo client is disconnected.
factory_remote.reset();
task_environment_.RunUntilIdle();
// The bucket context and the backing store should still exist.
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
// The last handle to the bucket context is released and the grace period
// elapses.
bucket_context_handle.Release();
task_environment_.FastForwardBy(base::Seconds(2));
VerifyBucketContext(bucket_id, /*expected_context_exists=*/false);
}
TEST_F(IndexedDBFactoryTest, PreCloseTasksStart) {
SetUpContext();
{
// Open a connection & immediately release it to cause the closing sequence
// to start.
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
storage::BucketId bucket_id = bucket_context_handle->bucket_locator().id;
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
BindIndexedDBFactory(std::move(checker_remote),
factory_remote.BindNewPipeAndPassReceiver(),
ToBucketInfo(bucket_context_handle->bucket_locator()));
bucket_context_handle.Release();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
EXPECT_TRUE(factory()->GetBucketContextForTesting(bucket_id)->IsClosing());
EXPECT_EQ(
IndexedDBBucketContext::ClosingState::kPreCloseGracePeriod,
factory()->GetBucketContextForTesting(bucket_id)->closing_stage());
task_environment_.FastForwardBy(base::Seconds(2));
// The factory should be closed, as the pre close tasks are delayed.
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/false);
}
// Move the clock to run the tasks in the next close sequence.
// NOTE: The constants rate-limiting sweeps and compaction are currently the
// same. This test may need to be restructured if these values diverge.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestGlobalSweepFromNow);
{
// Open a connection & immediately release it to cause the closing sequence
// to start again.
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
storage::BucketId bucket_id = bucket_context_handle->bucket_locator().id;
bucket_context_handle.Release();
// Manually execute the timer so that the PreCloseTaskList task doesn't also
// run.
factory()->GetBucketContextForTesting(bucket_id)->close_timer()->FireNow();
// The pre-close tasks should be running now.
ASSERT_TRUE(factory()->GetBucketContextForTesting(bucket_id));
EXPECT_EQ(
IndexedDBBucketContext::ClosingState::kRunningPreCloseTasks,
factory()->GetBucketContextForTesting(bucket_id)->closing_stage());
ASSERT_TRUE(factory()
->GetBucketContextForTesting(bucket_id)
->pre_close_task_queue());
EXPECT_TRUE(factory()
->GetBucketContextForTesting(bucket_id)
->pre_close_task_queue()
->started());
}
{
// Stop sweep by opening a connection.
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
storage::BucketId bucket_id = bucket_context_handle->bucket_locator().id;
EXPECT_FALSE(bucket_context_handle->pre_close_task_queue());
// Move clock forward to trigger next sweep, but storage key has longer
// sweep minimum, so no tasks should execute.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestGlobalSweepFromNow);
bucket_context_handle.Release();
ASSERT_TRUE(factory()->GetBucketContextForTesting(bucket_id));
EXPECT_EQ(
IndexedDBBucketContext::ClosingState::kPreCloseGracePeriod,
factory()->GetBucketContextForTesting(bucket_id)->closing_stage());
// Manually execute the timer so that the PreCloseTaskList task doesn't also
// run.
factory()->GetBucketContextForTesting(bucket_id)->close_timer()->FireNow();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
RunPostedTasks();
VerifyBucketContext(bucket_id, /*expected_context_exists=*/false);
}
{
// Finally, move the clock forward so the storage key should allow a sweep.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestBucketSweepFromNow);
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
storage::BucketId bucket_id = bucket_context_handle->bucket_locator().id;
bucket_context_handle.Release();
factory()->GetBucketContextForTesting(bucket_id)->close_timer()->FireNow();
ASSERT_TRUE(factory()->GetBucketContextForTesting(bucket_id));
EXPECT_EQ(
IndexedDBBucketContext::ClosingState::kRunningPreCloseTasks,
factory()->GetBucketContextForTesting(bucket_id)->closing_stage());
ASSERT_TRUE(factory()
->GetBucketContextForTesting(bucket_id)
->pre_close_task_queue());
EXPECT_TRUE(factory()
->GetBucketContextForTesting(bucket_id)
->pre_close_task_queue()
->started());
}
}
TEST_F(IndexedDBFactoryTest, TombstoneSweeperTiming) {
SetUpContext();
// Open a connection.
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
EXPECT_FALSE(bucket_context_handle->ShouldRunTombstoneSweeper());
// Move the clock to run the tasks in the next close sequence.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestGlobalSweepFromNow);
EXPECT_TRUE(bucket_context_handle->ShouldRunTombstoneSweeper());
// Move clock forward to trigger next sweep, but storage key has longer
// sweep minimum, so no tasks should execute.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestGlobalSweepFromNow);
EXPECT_FALSE(bucket_context_handle->ShouldRunTombstoneSweeper());
// Finally, move the clock forward so the storage key should allow a sweep.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestBucketSweepFromNow);
EXPECT_TRUE(bucket_context_handle->ShouldRunTombstoneSweeper());
}
TEST_F(IndexedDBFactoryTest, CompactionTaskTiming) {
SetUpContext();
// Open a connection.
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
bucket_context_handle->InitBackingStoreIfNeeded(/*create_if_missing=*/true);
EXPECT_FALSE(bucket_context_handle->ShouldRunCompaction());
// Move the clock to run the tasks in the next close sequence.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestGlobalCompactionFromNow);
EXPECT_TRUE(bucket_context_handle->ShouldRunCompaction());
// Move clock forward to trigger next compaction, but storage key has longer
// compaction minimum, so no tasks should execute.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestGlobalCompactionFromNow);
EXPECT_FALSE(bucket_context_handle->ShouldRunCompaction());
// Finally, move the clock forward so the storage key should allow a
// compaction.
task_environment_.FastForwardBy(
IndexedDBBucketContext::kMaxEarliestBucketCompactionFromNow);
EXPECT_TRUE(bucket_context_handle->ShouldRunCompaction());
}
TEST_F(IndexedDBFactoryTest, InMemoryFactoriesStay) {
SetUpInMemoryContext();
ASSERT_TRUE(context()->IsInMemoryContext());
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
storage::BucketLocator bucket_locator =
bucket_context_handle->bucket_locator();
EXPECT_TRUE(bucket_context_handle->backing_store()->in_memory());
bucket_context_handle.Release();
EXPECT_TRUE(factory()->GetBucketContextForTesting(bucket_locator.id));
EXPECT_FALSE(
factory()->GetBucketContextForTesting(bucket_locator.id)->IsClosing());
factory()->ForceClose(bucket_locator.id, false);
VerifyBucketContext(bucket_locator.id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
factory()->ForceClose(bucket_locator.id, true);
VerifyBucketContext(bucket_locator.id, /*expected_context_exists=*/false);
}
TEST_F(IndexedDBFactoryTest, TooLongOrigin) {
SetUpContext();
base::FilePath temp_dir =
context()->GetFirstPartyDataPathForTesting().DirName();
int limit = base::GetMaximumPathComponentLength(temp_dir);
EXPECT_GT(limit, 0);
std::string origin(limit + 1, 'x');
const blink::StorageKey too_long_storage_key =
blink::StorageKey::CreateFromStringForTesting("http://" + origin +
":81/");
storage::BucketInfo bucket_info = GetOrCreateBucket(
storage::BucketInitParams::ForDefaultBucket(too_long_storage_key));
storage::BucketLocator bucket_locator = bucket_info.ToBucketLocator();
IndexedDBBucketContextHandle bucket_context_handle(
factory()->GetOrCreateBucketContext(
ToBucketInfo(bucket_locator),
context()->GetDataPath(bucket_locator)));
leveldb::Status s;
std::tie(s, std::ignore, std::ignore) =
bucket_context_handle->InitBackingStoreIfNeeded(
/*create_if_missing=*/true);
EXPECT_TRUE(s.IsIOError());
}
TEST_F(IndexedDBFactoryTest, FactoryForceClose) {
SetUpContext();
IndexedDBBucketContextHandle bucket_context_handle = CreateBucketHandle();
storage::BucketLocator bucket_locator =
bucket_context_handle->bucket_locator();
bucket_context_handle->ForceClose(/*doom=*/false);
bucket_context_handle.Release();
VerifyBucketContext(bucket_locator.id, /*expected_context_exists=*/true,
/*expected_backing_store_exists=*/true);
RunPostedTasks();
VerifyBucketContext(bucket_locator.id, /*expected_context_exists=*/false);
}
// Tests that the backing store is closed when the connection is closed during
// upgrade.
TEST_F(IndexedDBFactoryTest, ConnectionCloseDuringUpgrade) {
SetUpContext();
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
auto bucket_locator = storage::BucketLocator();
bucket_locator.storage_key = storage_key;
// Bind the IDBFactory.
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
BindIndexedDBFactory(std::move(checker_remote),
factory_remote.BindNewPipeAndPassReceiver(),
ToBucketInfo(bucket_locator));
// Now create a database and thus the backing store.
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
mojo::PendingAssociatedRemote<blink::mojom::IDBDatabase> pending_database;
EXPECT_CALL(client, MockedUpgradeNeeded)
.WillOnce(
testing::DoAll(MoveArgPointee<0>(&pending_database),
::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(), u"db",
/*version=*/1,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/1);
run_loop.Run();
EXPECT_TRUE(factory()->GetBucketContextForTesting(bucket_locator.id));
EXPECT_FALSE(
factory()->GetBucketContextForTesting(bucket_locator.id)->IsClosing());
// Drop the connection.
pending_database.reset();
factory_remote.FlushForTesting();
EXPECT_TRUE(
factory()->GetBucketContextForTesting(bucket_locator.id)->IsClosing());
}
TEST_F(IndexedDBFactoryTest, DeleteDatabase) {
SetUpContext();
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
auto bucket_locator = storage::BucketLocator();
bucket_locator.storage_key = storage_key;
// Bind the IDBFactory.
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
BindIndexedDBFactory(std::move(checker_remote),
factory_remote.BindNewPipeAndPassReceiver(),
ToBucketInfo(bucket_locator));
// Don't create a backing store if one doesn't exist.
{
// Delete db.
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
EXPECT_CALL(client, DeleteSuccess)
.WillOnce(
testing::DoAll(::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->DeleteDatabase(client.CreateInterfacePtrAndBind(), u"db",
/*force_close=*/false);
run_loop.Run();
// Backing store shouldn't exist.
EXPECT_FALSE(factory()
->GetBucketContextForTesting(bucket_locator.id)
->backing_store());
}
// Now create a database and thus the backing store.
{
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
EXPECT_CALL(client, MockedOpenSuccess)
.WillOnce(::base::test::RunClosure(run_loop.QuitClosure()));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(), u"db",
/*version=*/0,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/1);
run_loop.Run();
}
// Delete the database now that the backing store actually exists.
{
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
EXPECT_CALL(client, DeleteSuccess)
.WillOnce(
testing::DoAll(::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->DeleteDatabase(client.CreateInterfacePtrAndBind(), u"db",
/*force_close=*/false);
run_loop.Run();
// Since there are no more references the factory should be closing.
ASSERT_TRUE(factory()->GetBucketContextForTesting(bucket_locator.id));
EXPECT_TRUE(
factory()->GetBucketContextForTesting(bucket_locator.id)->IsClosing());
}
}
TEST_F(IndexedDBFactoryTest, GetDatabaseNames_NoFactory) {
SetUpContext();
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
auto bucket_locator = storage::BucketLocator();
bucket_locator.storage_key = storage_key;
// Bind the IDBFactory.
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
BindIndexedDBFactory(std::move(checker_remote),
factory_remote.BindNewPipeAndPassReceiver(),
ToBucketInfo(bucket_locator));
// Don't create a backing store if one doesn't exist.
{
base::test::TestFuture<std::vector<blink::mojom::IDBNameAndVersionPtr>,
blink::mojom::IDBErrorPtr>
info_future;
factory_remote->GetDatabaseInfo(info_future.GetCallback());
ASSERT_TRUE(info_future.Wait());
EXPECT_FALSE(factory()
->GetBucketContextForTesting(bucket_locator.id)
->backing_store());
}
// Now create a database and thus the backing store.
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
// It's necessary to hang onto the database connection or the connection
// will shut itself down and the backing store will close on its own.
mojo::PendingAssociatedRemote<blink::mojom::IDBDatabase> pending_database;
EXPECT_CALL(client, MockedOpenSuccess)
.WillOnce(
testing::DoAll(MoveArgPointee<0>(&pending_database),
::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(), u"db",
/*version=*/0,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/1);
run_loop.Run();
// GetDatabaseInfo didn't create the factory, so it shouldn't close it.
{
base::test::TestFuture<std::vector<blink::mojom::IDBNameAndVersionPtr>,
blink::mojom::IDBErrorPtr>
info_future;
factory_remote->GetDatabaseInfo(info_future.GetCallback());
ASSERT_TRUE(info_future.Wait());
EXPECT_TRUE(factory()->GetBucketContextForTesting(bucket_locator.id));
EXPECT_FALSE(
factory()->GetBucketContextForTesting(bucket_locator.id)->IsClosing());
}
}
TEST_F(IndexedDBFactoryTest, QuotaErrorOnDiskFull) {
SetUpContext();
leveldb_env::SetDBFactoryForTesting(base::BindRepeating(
[](const leveldb_env::Options& options, const std::string& name,
std::unique_ptr<leveldb::DB>* dbptr) {
return leveldb_env::MakeIOError("foobar", "disk full",
leveldb_env::MethodID::kCreateDir,
base::File::FILE_ERROR_NO_SPACE);
}));
// Bind the IDBFactory.
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
auto bucket_locator = storage::BucketLocator();
bucket_locator.storage_key = storage_key;
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
BindIndexedDBFactory(std::move(checker_remote),
factory_remote.BindNewPipeAndPassReceiver(),
ToBucketInfo(bucket_locator));
// Expect an error when opening.
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
EXPECT_CALL(client, Error)
.WillOnce(
testing::DoAll(::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(), u"db",
/*version=*/1,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/1);
run_loop.Run();
// A disk full error results in an error reported to the quota system.
ASSERT_EQ(1U, quota_manager()->write_error_tracker().size());
EXPECT_EQ(storage_key, quota_manager()->write_error_tracker().begin()->first);
EXPECT_EQ(1, quota_manager()->write_error_tracker().begin()->second);
leveldb_env::SetDBFactoryForTesting({});
}
TEST_F(IndexedDBFactoryTest, DatabaseFailedOpen) {
SetUpContext();
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
auto bucket_locator = storage::BucketLocator();
bucket_locator.storage_key = storage_key;
const std::u16string db_name(u"db");
// Bind the IDBFactory.
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
BindIndexedDBFactory(std::move(checker_remote),
factory_remote.BindNewPipeAndPassReceiver(),
ToBucketInfo(bucket_locator));
// Open at version 2.
{
const int64_t db_version = 2;
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
EXPECT_CALL(client, MockedUpgradeNeeded)
.WillOnce(
testing::DoAll(::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(),
db_name, db_version,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/1);
run_loop.Run();
}
// Open at version < 2, which will fail.
{
const int64_t db_version = 1;
base::RunLoop run_loop;
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
EXPECT_CALL(client, Error)
.WillOnce(::base::test::RunClosure(run_loop.QuitClosure()));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(),
db_name, db_version,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/2);
run_loop.Run();
IndexedDBBucketContext* bucket_context =
factory()->GetBucketContextForTesting(bucket_locator.id);
ASSERT_TRUE(bucket_context);
EXPECT_FALSE(
base::Contains(bucket_context->GetDatabasesForTesting(), db_name));
}
}
// Test for `IndexedDBDataFormatVersion`.
TEST_F(IndexedDBFactoryTest, DataLoss) {
SetUpContext();
const blink::StorageKey storage_key =
blink::StorageKey::CreateFromStringForTesting("http://localhost:81");
auto bucket_locator = storage::BucketLocator();
bucket_locator.storage_key = storage_key;
const std::u16string db_name(u"test_db");
// Bind the IDBFactory.
mojo::Remote<blink::mojom::IDBFactory> factory_remote;
mojo::PendingRemote<storage::mojom::IndexedDBClientStateChecker>
checker_remote;
BindIndexedDBFactory(std::move(checker_remote),
factory_remote.BindNewPipeAndPassReceiver(),
ToBucketInfo(bucket_locator));
// Set a data format version and create a new database. No data loss.
{
base::AutoReset<IndexedDBDataFormatVersion> override_version(
&IndexedDBDataFormatVersion::GetMutableCurrentForTesting(),
IndexedDBDataFormatVersion(3, 4));
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
base::RunLoop run_loop;
EXPECT_CALL(client, MockedUpgradeNeeded(
_, _, blink::mojom::IDBDataLoss::None, _, _))
.WillOnce(
testing::DoAll(::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(),
db_name, /*version=*/1,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/1);
run_loop.Run();
// This step is necessary to make sure the backing store is closed so that
// the second `Open` will initialize it with the new (older) data format
// version. Without this step, the same `IndexedDBBackingStore` is reused
// because it's kept around for 2 seconds after the last connection is
// dropped.
base::RunLoop run_loop2;
context_->ForceClose(
bucket_locator.id,
storage::mojom::ForceCloseReason::FORCE_CLOSE_BACKING_STORE_FAILURE,
run_loop2.QuitClosure());
run_loop2.Run();
}
// Set an older data format version and try to reopen said database. Expect
// total data loss.
{
base::AutoReset<IndexedDBDataFormatVersion> override_version(
&IndexedDBDataFormatVersion::GetMutableCurrentForTesting(),
IndexedDBDataFormatVersion(3, 3));
base::RunLoop run_loop;
MockMojoIndexedDBFactoryClient client;
MockMojoIndexedDBDatabaseCallbacks database_callbacks;
EXPECT_CALL(client, MockedUpgradeNeeded(
_, _, blink::mojom::IDBDataLoss::Total, _, _))
.WillOnce(
testing::DoAll(::base::test::RunClosure(run_loop.QuitClosure())));
mojo::AssociatedRemote<blink::mojom::IDBTransaction> transaction_remote;
factory_remote->Open(client.CreateInterfacePtrAndBind(),
database_callbacks.CreateInterfacePtrAndBind(),
db_name, /*version=*/1,
transaction_remote.BindNewEndpointAndPassReceiver(),
/*transaction_id=*/2);
run_loop.Run();
}
}
} // namespace content