| // 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 |