| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <functional> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/files/file.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/lazy_instance.h" |
| #include "base/location.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/rand_util.h" |
| #include "base/run_loop.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.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_expected_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/run_until.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/test/thread_test_helper.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/time.h" |
| #include "build/build_config.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/privileged/mojom/indexed_db_control_test.mojom.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/blob_storage/chrome_blob_storage_context.h" |
| #include "content/browser/browser_main_loop.h" |
| #include "content/browser/indexed_db/indexed_db_leveldb_coding.h" |
| #include "content/browser/indexed_db/instance/bucket_context.h" |
| #include "content/browser/indexed_db/instance/leveldb/cleanup_scheduler.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/url_constants.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/no_renderer_crashes_assertion.h" |
| #include "content/shell/browser/shell.h" |
| #include "content/test/content_browser_test_utils_internal.h" |
| #include "net/base/net_errors.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "storage/browser/blob/blob_storage_context.h" |
| #include "storage/browser/quota/quota_manager.h" |
| #include "storage/browser/quota/quota_manager_proxy.h" |
| #include "storage/browser/quota/quota_settings.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "third_party/blink/public/common/switches.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| // These macros are used to temporarily disable specific tests for the SQLite |
| // backing store. |
| // TODO(crbug.com/419272072): Remove after implementing the missing pieces. |
| #define DISABLED_FOR_SQLITE_PENDING_FAILURE_INJECTION() \ |
| if (using_sqlite_) { \ |
| GTEST_SKIP() << "Pending failure injection"; \ |
| } |
| |
| using storage::QuotaManager; |
| using storage::mojom::FailClass; |
| using storage::mojom::FailMethod; |
| |
| namespace content::indexed_db { |
| |
| namespace { |
| |
| // Abstract base class that provides common functionality. Tests should use one |
| // of the concrete derived classes. |
| class IndexedDBBrowserTestBase : public ContentBrowserTest { |
| public: |
| // Derived test classes should specify whether their tests will run on the |
| // SQLite backing store or LevelDB. |
| explicit IndexedDBBrowserTestBase(bool use_sqlite) |
| : using_sqlite_(use_sqlite), |
| sqlite_override_( |
| BucketContext::OverrideShouldUseSqliteForTesting(use_sqlite)) {} |
| |
| IndexedDBBrowserTestBase(const IndexedDBBrowserTestBase&) = delete; |
| IndexedDBBrowserTestBase& operator=(const IndexedDBBrowserTestBase&) = delete; |
| |
| void SetUpOnMainThread() override { |
| // Some tests need more space than the default used for browser tests. |
| static storage::QuotaSettings quota_settings = |
| storage::GetHardCodedSettings(100 * 1024 * 1024); |
| StoragePartition::SetDefaultQuotaSettingsForTesting("a_settings); |
| SetUpMockFailureInjector(); |
| } |
| |
| virtual void SetUpMockFailureInjector() { |
| GetControlTest()->BindMockFailureSingletonForTesting( |
| failure_injector_.BindNewPipeAndPassReceiver()); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| // Enable experimental web platform features to enable write access. |
| command_line->AppendSwitch( |
| switches::kEnableExperimentalWebPlatformFeatures); |
| } |
| |
| void TearDownOnMainThread() override { failure_injector_.reset(); } |
| |
| bool UseProductionQuotaSettings() override { |
| // So that the browser test harness doesn't call |
| // SetDefaultQuotaSettingsForTesting and overwrite the settings above. |
| return true; |
| } |
| |
| void FailOperation(FailClass failure_class, |
| FailMethod failure_method, |
| int fail_on_instance_num, |
| int fail_on_call_num) { |
| failure_injector_->FailOperation(failure_class, failure_method, |
| fail_on_instance_num, fail_on_call_num); |
| } |
| |
| void SimpleTest(const GURL& test_url, Shell* shell = nullptr) { |
| // The test page will perform tests on IndexedDB, then navigate to either |
| // a #pass or #fail ref. |
| Shell* the_browser = shell ? shell : this->shell(); |
| |
| VLOG(0) << "Navigating to URL and blocking."; |
| NavigateToURLBlockUntilNavigationsComplete(the_browser, test_url, 2); |
| VLOG(0) << "Navigation done."; |
| std::string result = |
| the_browser->web_contents()->GetLastCommittedURL().ref(); |
| if (result != "pass") { |
| std::string js_result = EvalJs(the_browser, "getLog()").ExtractString(); |
| FAIL() << "Failed: " << js_result; |
| } |
| } |
| |
| void NavigateAndWaitForTitle(Shell* shell, |
| const char* filename, |
| const char* hash, |
| const char* expected_string) { |
| GURL url = GetTestUrl("indexeddb", filename); |
| if (hash) |
| url = GURL(url.spec() + hash); |
| |
| std::u16string expected_title16(base::ASCIIToUTF16(expected_string)); |
| TitleWatcher title_watcher(shell->web_contents(), expected_title16); |
| EXPECT_TRUE(NavigateToURL(shell, url)); |
| EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle()); |
| } |
| |
| storage::mojom::IndexedDBControl& GetControl(Shell* browser = nullptr) { |
| if (!browser) |
| browser = shell(); |
| return browser->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetIndexedDBControl(); |
| } |
| |
| mojo::Remote<storage::mojom::IndexedDBControlTest> GetControlTest( |
| Shell* browser = nullptr) { |
| mojo::Remote<storage::mojom::IndexedDBControlTest> idb_control_test; |
| BindControlTest(browser, idb_control_test.BindNewPipeAndPassReceiver()); |
| return idb_control_test; |
| } |
| |
| void BindControlTest( |
| Shell* browser, |
| mojo::PendingReceiver<storage::mojom::IndexedDBControlTest> receiver) { |
| if (!browser) { |
| browser = shell(); |
| } |
| browser->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetIndexedDBControl() |
| .BindTestInterfaceForTesting(std::move(receiver)); |
| } |
| |
| void SetQuota(int per_host_quota_kilobytes) { |
| SetTempQuota(per_host_quota_kilobytes, shell() |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetQuotaManager()); |
| } |
| |
| static void SetTempQuota(int per_host_quota_kilobytes, |
| scoped_refptr<QuotaManager> qm) { |
| if (!BrowserThread::CurrentlyOn(BrowserThread::IO)) { |
| GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&IndexedDBBrowserTestBase::SetTempQuota, |
| per_host_quota_kilobytes, qm)); |
| return; |
| } |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| const int KB = 1024; |
| qm->SetQuotaSettings( |
| storage::GetHardCodedSettings(per_host_quota_kilobytes * KB)); |
| } |
| |
| // Deletes the default bucket for `storage_key`, verifying the behavior of |
| // `IndexedDBContextImpl::DeleteBucketData`. |
| bool DeleteBucketData(const blink::StorageKey& storage_key, |
| Shell* browser = nullptr) { |
| base::test::TestFuture<blink::mojom::QuotaStatusCode> future; |
| (browser ? browser : shell()) |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetQuotaManager() |
| ->proxy() |
| ->DeleteBucket(storage_key, storage::kDefaultBucketName, |
| base::SequencedTaskRunner::GetCurrentDefault(), |
| future.GetCallback()); |
| return future.Take() == blink::mojom::QuotaStatusCode::kOk; |
| } |
| |
| int64_t RequestUsage(Shell* browser = nullptr) { |
| base::test::TestFuture<int64_t> future; |
| auto control = GetControlTest(browser); |
| control->GetUsageForTesting(future.GetCallback()); |
| return future.Take(); |
| } |
| |
| base::FilePath PathForBlob(const storage::BucketLocator& bucket_locator, |
| int64_t database_id, |
| int64_t blob_number) { |
| base::test::TestFuture<base::FilePath> future; |
| auto control_test = GetControlTest(); |
| control_test->GetPathForBlobForTesting( |
| bucket_locator, database_id, blob_number, |
| future.GetCallback<const base::FilePath&>()); |
| return future.Take(); |
| } |
| |
| int64_t GetBlobFileCount(const storage::BucketLocator& bucket_locator) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::FilePath blob_directory = |
| PathForBlob(bucket_locator, 1, 1).DirName().DirName().DirName(); |
| base::FileEnumerator file_enumerator(blob_directory, true, |
| base::FileEnumerator::FILES); |
| |
| int count = 0; |
| for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty(); |
| file_path = file_enumerator.Next()) { |
| count++; |
| } |
| return count; |
| } |
| |
| storage::QuotaErrorOr<storage::BucketInfo> GetOrCreateBucket( |
| const storage::BucketInitParams& params) { |
| base::test::TestFuture<storage::QuotaErrorOr<storage::BucketInfo>> future; |
| shell() |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetQuotaManager() |
| ->proxy() |
| ->UpdateOrCreateBucket( |
| params, base::SingleThreadTaskRunner::GetCurrentDefault(), |
| future.GetCallback()); |
| return future.Take(); |
| } |
| |
| protected: |
| bool using_sqlite_ = false; |
| base::AutoReset<std::optional<bool>> sqlite_override_; |
| mojo::Remote<storage::mojom::MockFailureInjector> failure_injector_; |
| }; |
| |
| // This browser test is aimed towards exercising the IndexedDB bindings and |
| // the actual implementation that lives in the browser side. |
| // The tests are parametrized to run with both the LevelDB (param = false) and |
| // SQLite (param = true) backing stores. |
| class IndexedDBBrowserTest : public IndexedDBBrowserTestBase, |
| public ::testing::WithParamInterface<bool> { |
| public: |
| IndexedDBBrowserTest() |
| : IndexedDBBrowserTestBase(/*use_sqlite=*/GetParam()) {} |
| }; |
| |
| // Tests that are applicable only to the LevelDB backing store. |
| class IndexedDBLevelDBOnlyTest : public IndexedDBBrowserTestBase { |
| public: |
| IndexedDBLevelDBOnlyTest() : IndexedDBBrowserTestBase(/*use_sqlite=*/false) {} |
| }; |
| |
| class IndexedDBIncognitoTest |
| : public IndexedDBBrowserTestBase, |
| public ::testing::WithParamInterface<std::tuple<bool, bool>> { |
| public: |
| IndexedDBIncognitoTest() |
| : IndexedDBBrowserTestBase(/*use_sqlite=*/std::get<0>(GetParam())) {} |
| |
| void SetUpMockFailureInjector() override { |
| if (IsIncognito()) { |
| shell_ = CreateOffTheRecordBrowser(); |
| GetControlTest(shell_)->BindMockFailureSingletonForTesting( |
| failure_injector_.BindNewPipeAndPassReceiver()); |
| } else { |
| IndexedDBBrowserTestBase::SetUpMockFailureInjector(); |
| } |
| } |
| |
| void TearDownOnMainThread() override { |
| shell_ = nullptr; |
| IndexedDBBrowserTestBase::TearDownOnMainThread(); |
| } |
| |
| bool IsIncognito() { return std::get<1>(GetParam()); } |
| |
| protected: |
| raw_ptr<Shell> shell_ = nullptr; |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBIncognitoTest, CursorTest) { |
| SimpleTest(GetTestUrl("indexeddb", "cursor_test.html"), shell_); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, CursorPrefetch) { |
| SimpleTest(GetTestUrl("indexeddb", "cursor_prefetch.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, IndexTest) { |
| SimpleTest(GetTestUrl("indexeddb", "index_test.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, KeyPathTest) { |
| SimpleTest(GetTestUrl("indexeddb", "key_path_test.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, TransactionGetTest) { |
| SimpleTest(GetTestUrl("indexeddb", "transaction_get_test.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, KeyTypesTest) { |
| SimpleTest(GetTestUrl("indexeddb", "key_types_test.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, ObjectStoreTest) { |
| base::HistogramTester tester; |
| |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.Open", 0); |
| |
| // This test opens a database and does 3 adds and 3 gets in the versionchange |
| // transaction (no readonly or readwrite transactions). |
| SimpleTest(GetTestUrl("indexeddb", "object_store_test.html")); |
| content::FetchHistogramsFromChildProcesses(); |
| |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.Open", 1); |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.ObjectStorePut", |
| 0); |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.ObjectStoreAdd", |
| 3); |
| // 2 of the adds succeed and one fails (due to the key already existing). |
| tester.ExpectBucketCount( |
| "WebCore.IndexedDB.RequestDispatchOutcome.ObjectStoreAdd", 1, 2); |
| tester.ExpectBucketCount( |
| "WebCore.IndexedDB.RequestDispatchOutcome.ObjectStoreAdd", 0, 1); |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.ObjectStoreGet", |
| 3); |
| tester.ExpectBucketCount( |
| "WebCore.IndexedDB.RequestDispatchOutcome.ObjectStoreGet", 1, 3); |
| |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadWrite.TimeQueued", |
| 0); |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadOnly.TimeQueued", |
| 0); |
| tester.ExpectTotalCount( |
| "WebCore.IndexedDB.Transaction.VersionChange.TimeQueued", 1); |
| |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadWrite.TimeActive2", |
| 0); |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadOnly.TimeActive2", |
| 0); |
| tester.ExpectTotalCount( |
| "WebCore.IndexedDB.Transaction.VersionChange.TimeActive2", 1); |
| |
| // This test opens a database and does 2 gets in one readonly transaction. |
| SimpleTest(GetTestUrl("indexeddb", "transaction_get_test.html")); |
| content::FetchHistogramsFromChildProcesses(); |
| |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.Open", 2); |
| tester.ExpectBucketCount("WebCore.IndexedDB.RequestDispatchOutcome.Open", 1, |
| 2); |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.ObjectStorePut", |
| 0); |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.ObjectStoreAdd", |
| 4); |
| // One more success than before. |
| tester.ExpectBucketCount( |
| "WebCore.IndexedDB.RequestDispatchOutcome.ObjectStoreAdd", 1, 3); |
| tester.ExpectTotalCount("WebCore.IndexedDB.RequestDuration2.ObjectStoreGet", |
| 5); |
| tester.ExpectBucketCount( |
| "WebCore.IndexedDB.RequestDispatchOutcome.ObjectStoreGet", 1, 5); |
| |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadWrite.TimeQueued", |
| 0); |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadOnly.TimeQueued", |
| 1); |
| tester.ExpectTotalCount( |
| "WebCore.IndexedDB.Transaction.VersionChange.TimeQueued", 2); |
| |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadWrite.TimeActive2", |
| 0); |
| tester.ExpectTotalCount("WebCore.IndexedDB.Transaction.ReadOnly.TimeActive2", |
| 1); |
| tester.ExpectTotalCount( |
| "WebCore.IndexedDB.Transaction.VersionChange.TimeActive2", 2); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, DatabaseTest) { |
| SimpleTest(GetTestUrl("indexeddb", "database_test.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, TransactionTest) { |
| SimpleTest(GetTestUrl("indexeddb", "transaction_test.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, CallbackAccounting) { |
| SimpleTest(GetTestUrl("indexeddb", "callback_accounting.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, DoesntHangTest) { |
| SimpleTest(GetTestUrl("indexeddb", "transaction_run_forever.html")); |
| CrashTab(shell()->web_contents()); |
| SimpleTest(GetTestUrl("indexeddb", "transaction_not_blocked.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, Bug84933Test) { |
| const GURL url = GetTestUrl("indexeddb", "bug_84933.html"); |
| |
| // Just navigate to the URL. Test will crash if it fails. |
| NavigateToURLBlockUntilNavigationsComplete(shell(), url, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, Bug106883Test) { |
| const GURL url = GetTestUrl("indexeddb", "bug_106883.html"); |
| |
| // Just navigate to the URL. Test will crash if it fails. |
| NavigateToURLBlockUntilNavigationsComplete(shell(), url, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, Bug109187Test) { |
| const GURL url = GetTestUrl("indexeddb", "bug_109187.html"); |
| |
| // Just navigate to the URL. Test will crash if it fails. |
| NavigateToURLBlockUntilNavigationsComplete(shell(), url, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, Bug941965Test) { |
| if (using_sqlite_) { |
| GTEST_SKIP() << "Flaky with SQLite, see crbug.com/435459644"; |
| } |
| |
| // Double-open an incognito window to test that saving & reading a blob from |
| // indexeddb works. |
| Shell* incognito_browser = CreateOffTheRecordBrowser(); |
| SimpleTest(GetTestUrl("indexeddb", "simple_blob_read.html"), |
| incognito_browser); |
| ASSERT_TRUE(incognito_browser); |
| incognito_browser->Close(); |
| incognito_browser = CreateOffTheRecordBrowser(); |
| SimpleTest(GetTestUrl("indexeddb", "simple_blob_read.html"), |
| incognito_browser); |
| ASSERT_TRUE(incognito_browser); |
| incognito_browser->Close(); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, SchedulingPriority) { |
| // This test page just opens a connection. |
| const GURL url = GetTestUrl("indexeddb", "simple_test.html"); |
| NavigateToURLBlockUntilNavigationsComplete(shell(), url, 1); |
| auto control_test = GetControlTest(); |
| |
| // This test could use a TestFuture inside RunUntil, but Mac doesn't like that |
| // type of message loop nesting. Therefore the RunUntils below asynchronously |
| // update this variable using this closure, and the value returned, if any, |
| // will be checked on the next iteration of the RunUntil body. |
| std::optional<int> priority; |
| base::RepeatingCallback<void(std::optional<int>)> update_priority = |
| base::BindLambdaForTesting( |
| [&priority](std::optional<int> fetched_priority) { |
| priority = fetched_priority; |
| }); |
| |
| // Since the test page is foregrounded/visible, it should get a priority of |
| // 0. |
| ASSERT_TRUE(base::test::RunUntil([&]() { |
| if (priority == 0) { |
| return true; |
| } |
| control_test->GetSchedulingPriorityForTesting(update_priority); |
| return false; |
| })); |
| |
| // This part is just designed to flush out any pending |
| // `GetSchedulingPriorityForTesting()` calls. `control_test.FlushForTesting()` |
| // would be sufficient except that its implementation is also async. |
| base::test::TestFuture<std::optional<int>> future; |
| control_test->GetSchedulingPriorityForTesting(future.GetCallback()); |
| EXPECT_TRUE(future.Wait()); |
| priority.reset(); |
| |
| // Hide the page and wait for the update to come through. |
| shell()->web_contents()->UpdateWebContentsVisibility(Visibility::HIDDEN); |
| |
| ASSERT_TRUE(base::test::RunUntil([&]() { |
| if (priority && *priority > 0) { |
| return true; |
| } |
| control_test->GetSchedulingPriorityForTesting(update_priority); |
| return false; |
| })); |
| } |
| |
| class IndexedDBBrowserTestWithLowQuota : public IndexedDBBrowserTest { |
| public: |
| IndexedDBBrowserTestWithLowQuota() = default; |
| |
| IndexedDBBrowserTestWithLowQuota(const IndexedDBBrowserTestWithLowQuota&) = |
| delete; |
| IndexedDBBrowserTestWithLowQuota& operator=( |
| const IndexedDBBrowserTestWithLowQuota&) = delete; |
| |
| void SetUpOnMainThread() override { |
| const int kInitialQuotaKilobytes = 5000; |
| SetQuota(kInitialQuotaKilobytes); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTestWithLowQuota, QuotaTest) { |
| SimpleTest(GetTestUrl("indexeddb", "quota_test.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTestWithLowQuota, QuotaTestWithCommit) { |
| SimpleTest(GetTestUrl("indexeddb", "bug_1203335.html")); |
| } |
| |
| class IndexedDBBrowserTestWithGCExposed : public IndexedDBBrowserTest { |
| public: |
| IndexedDBBrowserTestWithGCExposed() = default; |
| |
| IndexedDBBrowserTestWithGCExposed(const IndexedDBBrowserTestWithGCExposed&) = |
| delete; |
| IndexedDBBrowserTestWithGCExposed& operator=( |
| const IndexedDBBrowserTestWithGCExposed&) = delete; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| command_line->AppendSwitchASCII(blink::switches::kJavaScriptFlags, |
| "--expose-gc"); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTestWithGCExposed, |
| DatabaseCallbacksTest) { |
| SimpleTest(GetTestUrl("indexeddb", "database_callbacks_first.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTestWithGCExposed, Bug346955148Test) { |
| SimpleTest(GetTestUrl("indexeddb", "bug_346955148.html")); |
| } |
| |
| // Regression test for crbug.com/392376370 |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTestWithGCExposed, NestedBlob) { |
| SimpleTest(GetTestUrl("indexeddb", "nested_blob.html")); |
| } |
| |
| struct BlobModificationTime { |
| base::FilePath relative_blob_path; |
| base::Time time; |
| }; |
| |
| static void CopyLevelDBToProfile( |
| Shell* shell, |
| const base::FilePath& data_path, |
| const std::string& test_directory, |
| std::vector<BlobModificationTime> modification_times) { |
| base::FilePath leveldb_dir(FILE_PATH_LITERAL("file__0.indexeddb.leveldb")); |
| base::FilePath blob_dir(FILE_PATH_LITERAL("file__0.indexeddb.blob")); |
| base::FilePath test_leveldb_data_dir = |
| GetTestFilePath("indexeddb", test_directory.c_str()).Append(leveldb_dir); |
| base::FilePath test_blob_data_dir = |
| GetTestFilePath("indexeddb", test_directory.c_str()).Append(blob_dir); |
| base::FilePath leveldb_dest = data_path.Append(leveldb_dir); |
| base::FilePath blob_dest = data_path.Append(blob_dir); |
| // If we don't create the destination directories first, the contents of the |
| // leveldb directory are copied directly into profile/IndexedDB instead of |
| // profile/IndexedDB/file__0.xxx/ |
| ASSERT_TRUE(base::CreateDirectory(leveldb_dest)); |
| const bool kRecursive = true; |
| ASSERT_TRUE( |
| base::CopyDirectory(test_leveldb_data_dir, data_path, kRecursive)); |
| |
| if (!base::PathExists(test_blob_data_dir)) |
| return; |
| ASSERT_TRUE(base::CreateDirectory(blob_dest)); |
| ASSERT_TRUE(base::CopyDirectory(test_blob_data_dir, data_path, kRecursive)); |
| // For some reason touching files on Android fails with EPERM. |
| // https://crbug.com/1045488 |
| #if !BUILDFLAG(IS_ANDROID) |
| // The modification time of the saved blobs is used for File objects, so these |
| // need to manually be set (they are clobbered both by the above copy |
| // operation and by git). |
| for (const BlobModificationTime& time : modification_times) { |
| base::FilePath total_path = blob_dest.Append(time.relative_blob_path); |
| ASSERT_TRUE(base::TouchFile(total_path, time.time, time.time)); |
| } |
| #endif |
| } |
| |
| class IndexedDBBrowserTestWithPreexistingLevelDB |
| : public IndexedDBLevelDBOnlyTest { |
| public: |
| IndexedDBBrowserTestWithPreexistingLevelDB() = default; |
| |
| IndexedDBBrowserTestWithPreexistingLevelDB( |
| const IndexedDBBrowserTestWithPreexistingLevelDB&) = delete; |
| IndexedDBBrowserTestWithPreexistingLevelDB& operator=( |
| const IndexedDBBrowserTestWithPreexistingLevelDB&) = delete; |
| |
| void SetUpOnMainThread() override { |
| base::RunLoop loop_move; |
| auto control_test = GetControlTest(); |
| control_test->GetBaseDataPathForTesting( |
| base::BindLambdaForTesting([&](const base::FilePath& data_path) { |
| CopyLevelDBToProfile(shell(), data_path, EnclosingLevelDBDir(), |
| CustomModificationTimes()); |
| loop_move.Quit(); |
| })); |
| loop_move.Run(); |
| base::RunLoop loop_init; |
| control_test->ForceInitializeFromFilesForTesting( |
| base::BindLambdaForTesting([&]() { loop_init.Quit(); })); |
| loop_init.Run(); |
| } |
| |
| virtual std::string EnclosingLevelDBDir() = 0; |
| |
| virtual std::vector<BlobModificationTime> CustomModificationTimes() { |
| return std::vector<BlobModificationTime>(); |
| } |
| }; |
| |
| class IndexedDBBrowserTestWithVersion3Schema |
| : public IndexedDBBrowserTestWithPreexistingLevelDB { |
| std::string EnclosingLevelDBDir() override { return "v3_migration_test"; } |
| |
| std::vector<BlobModificationTime> CustomModificationTimes() override { |
| return { |
| {base::FilePath(FILE_PATH_LITERAL("1/00/3")), |
| base::Time::FromMillisecondsSinceUnixEpoch(1579809038000)}, |
| {base::FilePath(FILE_PATH_LITERAL("1/00/4")), |
| base::Time::FromMillisecondsSinceUnixEpoch(1579808985000)}, |
| {base::FilePath(FILE_PATH_LITERAL("1/00/5")), |
| base::Time::FromMillisecondsSinceUnixEpoch(1579199256000)}, |
| }; |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithVersion3Schema, MigrationTest) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "v3_migration_test.html"); |
| // For some reason setting empty file modification time on Android fails with |
| // EPERM. https://crbug.com/1045488 |
| #if BUILDFLAG(IS_ANDROID) |
| SimpleTest(GURL(kTestUrl.spec() + "#ignoreTimes")); |
| #else |
| SimpleTest(kTestUrl); |
| #endif |
| } |
| |
| class IndexedDBBrowserTestWithVersion123456Schema : public |
| IndexedDBBrowserTestWithPreexistingLevelDB { |
| std::string EnclosingLevelDBDir() override { return "schema_version_123456"; } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithVersion123456Schema, |
| DestroyTest) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "open_bad_db.html"); |
| int64_t original_size = RequestUsage(); |
| EXPECT_GT(original_size, 0); |
| SimpleTest(kTestUrl); |
| int64_t new_size = RequestUsage(); |
| EXPECT_GT(new_size, 0); |
| EXPECT_NE(original_size, new_size); |
| } |
| |
| class IndexedDBBrowserTestWithVersion987654SSVData : public |
| IndexedDBBrowserTestWithPreexistingLevelDB { |
| std::string EnclosingLevelDBDir() override { return "ssv_version_987654"; } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithVersion987654SSVData, |
| DestroyTest) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "open_bad_db.html"); |
| int64_t original_size = RequestUsage(); |
| EXPECT_GT(original_size, 0); |
| SimpleTest(kTestUrl); |
| int64_t new_size = RequestUsage(); |
| EXPECT_GT(new_size, 0); |
| EXPECT_NE(original_size, new_size); |
| } |
| |
| class IndexedDBBrowserTestsWithCleanupScheduler |
| : public IndexedDBLevelDBOnlyTest { |
| public: |
| IndexedDBBrowserTestsWithCleanupScheduler() { |
| scoped_feature_list_.InitAndEnableFeature( |
| content::indexed_db::level_db::kIdbInSessionDbCleanup); |
| } |
| |
| protected: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Regression test for crbug.com/413540372. |
| // More details in `index_deletion_regression_tests.js`. |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestsWithCleanupScheduler, |
| RollbackTrasactionDuringTombstoneSweep) { |
| const GURL kTestUrl = |
| GetTestUrl("indexeddb", "index_deletion_regression_tests.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), kTestUrl)); |
| |
| int delay_for_sweeper_run = |
| content::indexed_db::level_db::LevelDBCleanupScheduler::kDeferTime |
| .InMilliseconds() + |
| 100; |
| int num_entries = content::indexed_db::level_db::LevelDBCleanupScheduler:: |
| kTombstoneThreshold + |
| 1; |
| ASSERT_TRUE( |
| ExecJs(shell(), base::StringPrintf("runRollbackTest(%d, %d)", num_entries, |
| delay_for_sweeper_run))); |
| } |
| |
| // Regression test for crbug.com/413540372. |
| // More details in `index_deletion_regression_tests.js`. |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestsWithCleanupScheduler, |
| TransactionInterleavedWithSweeper) { |
| const GURL kTestUrl = |
| GetTestUrl("indexeddb", "index_deletion_regression_tests.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), kTestUrl)); |
| |
| int num_entries = content::indexed_db::level_db::LevelDBCleanupScheduler:: |
| kTombstoneThreshold + |
| 1; |
| int delay_before_interleaved_updates = |
| content::indexed_db::level_db::LevelDBCleanupScheduler::kDeferTime |
| .InMilliseconds() + |
| 100; |
| int delay_to_finish_sweeper = |
| content::indexed_db::level_db::LevelDBCleanupScheduler::kDeferTime |
| .InMilliseconds() * |
| 5; |
| |
| ASSERT_TRUE(ExecJs( |
| shell(), base::StringPrintf("runInterleavedTest(%d, %d, %d)", num_entries, |
| delay_before_interleaved_updates, |
| delay_to_finish_sweeper))); |
| } |
| |
| class IndexedDBBrowserTestWithCorruptLevelDB : public |
| IndexedDBBrowserTestWithPreexistingLevelDB { |
| std::string EnclosingLevelDBDir() override { return "corrupt_leveldb"; } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithCorruptLevelDB, |
| DestroyTest) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "open_bad_db.html"); |
| int64_t original_size = RequestUsage(); |
| EXPECT_GT(original_size, 0); |
| SimpleTest(kTestUrl); |
| int64_t new_size = RequestUsage(); |
| EXPECT_GT(new_size, 0); |
| EXPECT_NE(original_size, new_size); |
| } |
| |
| class IndexedDBBrowserTestWithMissingSSTFile : public |
| IndexedDBBrowserTestWithPreexistingLevelDB { |
| std::string EnclosingLevelDBDir() override { return "missing_sst"; } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithMissingSSTFile, |
| DestroyTest) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "open_missing_table.html"); |
| int64_t original_size = RequestUsage(); |
| EXPECT_GT(original_size, 0); |
| SimpleTest(kTestUrl); |
| int64_t new_size = RequestUsage(); |
| EXPECT_GT(new_size, 0); |
| EXPECT_NE(original_size, new_size); |
| } |
| |
| // IndexedDBBrowserTestWithCrbug899446* capture IDB instances from Chrome stable |
| // to verify that the current code can read those instances. For more info on |
| // a case when Chrome canary couldn't read stable's IDB instances, see |
| // https://crbug.com/899446. |
| |
| class IndexedDBBrowserTestWithCrbug899446 |
| : public IndexedDBBrowserTestWithPreexistingLevelDB { |
| std::string EnclosingLevelDBDir() override { return "crbug899446"; } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithCrbug899446, StableTest) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "crbug899446.html"); |
| SimpleTest(kTestUrl); |
| } |
| |
| class IndexedDBBrowserTestWithCrbug899446Noai |
| : public IndexedDBBrowserTestWithPreexistingLevelDB { |
| std::string EnclosingLevelDBDir() override { return "crbug899446_noai"; } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestWithCrbug899446Noai, StableTest) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "crbug899446_noai.html"); |
| SimpleTest(kTestUrl); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(IndexedDBLevelDBOnlyTest, LevelDBLogFileTest) { |
| // Any page that opens an IndexedDB will work here. |
| SimpleTest(GetTestUrl("indexeddb", "database_test.html")); |
| base::FilePath leveldb_dir(FILE_PATH_LITERAL("file__0.indexeddb.leveldb")); |
| base::FilePath log_file(FILE_PATH_LITERAL("LOG")); |
| |
| base::FilePath log_file_path; |
| base::RunLoop loop; |
| auto control_test = GetControlTest(); |
| control_test->GetBaseDataPathForTesting( |
| base::BindLambdaForTesting([&](const base::FilePath& path) { |
| log_file_path = path.Append(leveldb_dir).Append(log_file); |
| loop.Quit(); |
| })); |
| loop.Run(); |
| |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| std::optional<int64_t> size = base::GetFileSize(log_file_path); |
| ASSERT_TRUE(size.has_value()); |
| EXPECT_GT(size.value(), 0); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, CanDeleteWhenOverQuotaTest) { |
| // The test needs to successfully add at least one record to the object store |
| // so that it can verify that the record can be deleted when over quota. The |
| // disk usage of a database with one object store and record is ~80KB when |
| // backed by SQLite but much lower with LevelDB, so set the initial quota |
| // accordingly. |
| constexpr int kInitialQuotaKilobytesForLevelDb = 5; |
| constexpr int kInitialQuotaKilobytesForSqlite = 100; |
| SetQuota(using_sqlite_ ? kInitialQuotaKilobytesForSqlite |
| : kInitialQuotaKilobytesForLevelDb); |
| const GURL kTestUrl = GetTestUrl("indexeddb", "fill_quota.html"); |
| SimpleTest(kTestUrl); |
| SetQuota(1); |
| SimpleTest(GetTestUrl("indexeddb", "delete_over_quota.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, EmptyBlob) { |
| // First delete all IDB's for the test storage_key |
| const GURL kTestUrl = GetTestUrl("indexeddb", "empty_blob.html"); |
| const blink::StorageKey kTestStorageKey = |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(kTestUrl)); |
| DeleteBucketData(kTestStorageKey); |
| ASSERT_OK_AND_ASSIGN( |
| const auto bucket_info, |
| GetOrCreateBucket( |
| storage::BucketInitParams::ForDefaultBucket(kTestStorageKey))); |
| const auto bucket_locator = bucket_info.ToBucketLocator(); |
| EXPECT_EQ(0, |
| GetBlobFileCount(bucket_locator)); // Start with no blob files. |
| // For some reason Android's futimes fails (EPERM) in this test. Do not assert |
| // file times on Android, but do so on other platforms. crbug.com/467247 |
| // TODO(cmumford): Figure out why this is the case and fix if possible. |
| #if BUILDFLAG(IS_ANDROID) |
| SimpleTest(GURL(kTestUrl.spec() + "#ignoreTimes")); |
| #else |
| SimpleTest(kTestUrl); |
| #endif |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, BlobsCountAgainstQuota) { |
| if (using_sqlite_) { |
| // TODO(crbug.com/433318798): Enable this test after reclaiming disk space |
| // on data deletion. |
| GTEST_SKIP(); |
| } |
| SimpleTest(GetTestUrl("indexeddb", "blobs_use_quota.html")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, DeleteBucketDataDeletesBlobs) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "write_4mb_blob.html"); |
| const blink::StorageKey kTestStorageKey = |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(kTestUrl)); |
| SimpleTest(kTestUrl); |
| int64_t size = RequestUsage(); |
| // This assertion assumes that we do not compress blobs. |
| EXPECT_GT(size, 4 << 20 /* 4 MB */); |
| DeleteBucketData(kTestStorageKey); |
| EXPECT_EQ(0, RequestUsage()); |
| } |
| |
| // Regression test for crbug.com/330868483 |
| // In this test, |
| // 1. the page reads a blob |
| // 2. the backing store is force-closed |
| // 3. the reference to the blob is GC'd |
| // . this disconnects the IndexedDBDataItemReader *after* the backing store |
| // is already reset |
| // 4. the page reads the same blob, reusing the IndexedDBDataItemReader |
| // 5. the blob reference is dropped and GC'd again |
| // 6. don't crash |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTestWithGCExposed, ForceCloseWithBlob) { |
| if (using_sqlite_) { |
| // TODO(crbug.com/419208485): Enable this test after handling active blob |
| // references during force close. |
| GTEST_SKIP(); |
| } |
| const GURL kTestUrl = GetTestUrl("indexeddb", "write_and_read_blob.html"); |
| SimpleTest(kTestUrl); |
| DeleteBucketData( |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(kTestUrl))); |
| std::ignore = EvalJs(shell(), "gc()"); |
| |
| // Run the test again, but don't reset the object stores first to make sure |
| // the same blob is read again. |
| std::ignore = EvalJs(shell(), "testThenGc()"); |
| while (true) { |
| std::string result = shell()->web_contents()->GetLastCommittedURL().ref(); |
| if (!result.empty()) { |
| EXPECT_EQ(result, "pass"); |
| break; |
| } |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, DeleteBucketDataIncognito) { |
| const GURL test_url = GetTestUrl("indexeddb", "fill_up_5k.html"); |
| const blink::StorageKey kTestStorageKey = |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(test_url)); |
| |
| Shell* browser = CreateOffTheRecordBrowser(); |
| SimpleTest(test_url, browser); |
| |
| EXPECT_GT(RequestUsage(browser), 5 * 1024); |
| |
| DeleteBucketData(kTestStorageKey, browser); |
| |
| EXPECT_EQ(0, RequestUsage(browser)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, DiskFullOnCommit) { |
| DISABLED_FOR_SQLITE_PENDING_FAILURE_INJECTION(); |
| // Ignore several preceding transactions: |
| // * The test calls deleteDatabase() which opens the backing store: |
| // #1: Transaction::Commit - initial "versionchange" transaction |
| // * Once the connection is opened, the test runs: |
| // #2: Transaction::Commit - the test's "readwrite" transaction) |
| const int instance_num = 2; |
| const int call_num = 1; |
| FailOperation(FailClass::LEVELDB_TRANSACTION, FailMethod::COMMIT_DISK_FULL, |
| instance_num, call_num); |
| SimpleTest(GetTestUrl("indexeddb", "disk_full_on_commit.html")); |
| } |
| |
| std::unique_ptr<net::test_server::HttpResponse> ServePath( |
| std::string request_path) { |
| base::FilePath resource_path = |
| content::GetTestFilePath("indexeddb", request_path.c_str()); |
| auto http_response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_OK); |
| |
| std::string file_contents; |
| if (!base::ReadFileToString(resource_path, &file_contents)) { |
| NOTREACHED() << "could not read file " << resource_path; |
| } |
| http_response->set_content(file_contents); |
| return std::move(http_response); |
| } |
| |
| #if !BUILDFLAG(IS_WIN) && !BUILDFLAG(IS_IOS) |
| void CorruptDatabase(const base::FilePath& idb_data_path) { |
| int num_files = 0; |
| int num_errors = 0; |
| const bool recursive = false; |
| |
| base::FileEnumerator enumerator(idb_data_path, recursive, |
| base::FileEnumerator::FILES); |
| for (base::FilePath idb_file = enumerator.Next(); !idb_file.empty(); |
| idb_file = enumerator.Next()) { |
| int64_t size = base::GetFileSize(idb_file).value_or(0); |
| |
| if (idb_file.Extension() == FILE_PATH_LITERAL(".ldb")) { |
| num_files++; |
| base::File file(idb_file, |
| base::File::FLAG_WRITE | base::File::FLAG_OPEN_TRUNCATED); |
| if (file.IsValid()) { |
| // Was opened truncated, expand back to the original |
| // file size and fill with zeros (corrupting the file). |
| file.SetLength(size); |
| } else { |
| num_errors++; |
| } |
| } |
| } |
| VLOG(0) << "There were " << num_files << " in " << idb_data_path.value() |
| << " with " << num_errors << " errors"; |
| } |
| |
| const char s_corrupt_db_test_prefix[] = "/corrupt/test/"; |
| |
| std::unique_ptr<net::test_server::HttpResponse> CorruptDBRequestHandler( |
| scoped_refptr<base::SequencedTaskRunner> task_runner, |
| const storage::BucketLocator& bucket_locator, |
| const std::string& path, |
| IndexedDBBrowserTestBase* test, |
| const net::test_server::HttpRequest& request) { |
| std::string request_path; |
| if (path.find(s_corrupt_db_test_prefix) == std::string::npos) |
| return nullptr; |
| |
| request_path = |
| request.relative_url.substr(std::string(s_corrupt_db_test_prefix).size()); |
| |
| // Remove the query string if present. |
| std::string request_query; |
| size_t query_pos = request_path.find('?'); |
| if (query_pos != std::string::npos) { |
| request_query = request_path.substr(query_pos + 1); |
| request_path = request_path.substr(0, query_pos); |
| } |
| |
| if (request_path == "corruptdb" && !request_query.empty()) { |
| VLOG(0) << "Requested to corrupt IndexedDB: " << request_query; |
| |
| // BindControlTest must be called on the same sequence that |
| // IndexedDBBrowserTestBase lives on. |
| mojo::Remote<storage::mojom::IndexedDBControlTest> control_test; |
| task_runner->PostTask( |
| FROM_HERE, base::BindOnce(&IndexedDBBrowserTestBase::BindControlTest, |
| base::Unretained(test), nullptr, |
| control_test.BindNewPipeAndPassReceiver())); |
| |
| // TODO(enne): this is a nested message loop on the embedded test server's |
| // IO thread. Windows does not support such nested message loops. |
| // However, alternatives like WaitableEvent can't be used here because |
| // these potentially cross-process mojo calls have callbacks that will |
| // bounce through the IO thread, causing a deadlock if we wait here. |
| // The ideal solution here is to refactor the embedded test server |
| // to support asynchronous request handlers (if possible??). |
| // The less ideal temporary solution is to only run these tests on |
| // non-Windows. |
| base::RunLoop loop; |
| control_test->FlushBackingStoreForTesting( |
| bucket_locator, base::BindLambdaForTesting([&]() { |
| control_test->GetFilePathForTesting( |
| bucket_locator, |
| base::BindLambdaForTesting([&](const base::FilePath& path) { |
| CorruptDatabase(path); |
| loop.Quit(); |
| })); |
| })); |
| loop.Run(); |
| |
| auto http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_OK); |
| return std::move(http_response); |
| } |
| |
| if (request_path == "fail" && !request_query.empty()) { |
| FailClass failure_class = FailClass::NOTHING; |
| FailMethod failure_method = FailMethod::NOTHING; |
| int instance_num = 1; |
| int call_num = 1; |
| std::string fail_class; |
| std::string fail_method; |
| |
| url::Component query(0, request_query.length()), key_pos, value_pos; |
| while (url::ExtractQueryKeyValue(request_query, &query, &key_pos, |
| &value_pos)) { |
| std::string escaped_key(request_query.substr(key_pos.begin, key_pos.len)); |
| std::string escaped_value( |
| request_query.substr(value_pos.begin, value_pos.len)); |
| |
| std::string key = base::UnescapeBinaryURLComponent(escaped_key); |
| |
| std::string value = base::UnescapeBinaryURLComponent(escaped_value); |
| |
| if (key == "method") { |
| fail_method = value; |
| } else if (key == "class") { |
| fail_class = value; |
| } else if (key == "instNum") { |
| instance_num = atoi(value.c_str()); |
| } else if (key == "callNum") { |
| call_num = atoi(value.c_str()); |
| } else { |
| NOTREACHED() << "Unknown param: \"" << key << "\""; |
| } |
| } |
| |
| if (fail_class == "LevelDBTransaction") { |
| failure_class = FailClass::LEVELDB_TRANSACTION; |
| if (fail_method == "Get") { |
| failure_method = FailMethod::GET; |
| } else if (fail_method == "Commit") { |
| failure_method = FailMethod::COMMIT; |
| } else { |
| NOTREACHED() << "Unknown method: \"" << fail_method << "\""; |
| } |
| } else if (fail_class == "LevelDBIterator") { |
| failure_class = FailClass::LEVELDB_ITERATOR; |
| if (fail_method == "Seek") { |
| failure_method = FailMethod::SEEK; |
| } else { |
| NOTREACHED() << "Unknown method: \"" << fail_method << "\""; |
| } |
| } else if (fail_class == "LevelDBDatabase") { |
| failure_class = FailClass::LEVELDB_DATABASE; |
| if (fail_method == "Write") { |
| failure_method = FailMethod::WRITE; |
| } else { |
| NOTREACHED() << "Unknown method: \"" << fail_method << "\""; |
| } |
| } else if (fail_class == "LevelDBDirectTransaction") { |
| failure_class = FailClass::LEVELDB_DIRECT_TRANSACTION; |
| if (fail_method == "Get") { |
| failure_method = FailMethod::GET; |
| } else { |
| NOTREACHED() << "Unknown method: \"" << fail_method << "\""; |
| } |
| } else { |
| NOTREACHED() << "Unknown class: \"" << fail_class << "\""; |
| } |
| |
| DCHECK_GE(instance_num, 1); |
| DCHECK_GE(call_num, 1); |
| |
| task_runner->PostTask( |
| FROM_HERE, base::BindOnce(&IndexedDBBrowserTestBase::FailOperation, |
| base::Unretained(test), failure_class, |
| failure_method, instance_num, call_num)); |
| |
| auto http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_code(net::HTTP_OK); |
| return std::move(http_response); |
| } |
| |
| return ServePath(request_path); |
| } |
| #endif |
| |
| const char s_indexeddb_test_prefix[] = "/indexeddb/test/"; |
| |
| std::unique_ptr<net::test_server::HttpResponse> StaticFileRequestHandler( |
| const std::string& path, |
| const net::test_server::HttpRequest& request) { |
| if (path.find(s_indexeddb_test_prefix) == std::string::npos) |
| return nullptr; |
| std::string request_path = |
| request.relative_url.substr(std::string(s_indexeddb_test_prefix).size()); |
| return ServePath(request_path); |
| } |
| |
| // TODO(crbug.com/419272072): Adapt this test suite to also work with the SQLite |
| // backing store. |
| // See TODO in CorruptDBRequestHandler. Windows does not support nested |
| // message loops on the IO thread, so run this test on other platforms. |
| // iOS runs into difficulty with the nested IO message loop as well. |
| #if !BUILDFLAG(IS_WIN) && !BUILDFLAG(IS_IOS) |
| class IndexedDBBrowserTestWithCorruption |
| : public IndexedDBLevelDBOnlyTest, |
| public ::testing::WithParamInterface<const char*> {}; |
| |
| INSTANTIATE_TEST_SUITE_P(/* no prefix */, |
| IndexedDBBrowserTestWithCorruption, |
| ::testing::Values("failGetBlobJournal", |
| "get", |
| "getAll", |
| "iterate", |
| "failTransactionCommit", |
| "clearObjectStore")); |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTestWithCorruption, |
| OperationOnCorruptedOpenDatabase) { |
| ASSERT_TRUE(embedded_test_server()->Started() || |
| embedded_test_server()->InitializeAndListen()); |
| const blink::StorageKey storage_key = blink::StorageKey::CreateFirstParty( |
| url::Origin::Create(embedded_test_server()->base_url())); |
| base::RunLoop loop; |
| storage::BucketLocator bucket_locator; |
| shell() |
| ->web_contents() |
| ->GetBrowserContext() |
| ->GetDefaultStoragePartition() |
| ->GetQuotaManager() |
| ->proxy() |
| ->UpdateOrCreateBucket( |
| storage::BucketInitParams::ForDefaultBucket(storage_key), |
| base::SequencedTaskRunner::GetCurrentDefault(), |
| base::BindLambdaForTesting( |
| [&](storage::QuotaErrorOr<storage::BucketInfo> result) { |
| ASSERT_TRUE(result.has_value()); |
| bucket_locator = result->ToBucketLocator(); |
| loop.Quit(); |
| })); |
| loop.Run(); |
| embedded_test_server()->RegisterRequestHandler(base::BindRepeating( |
| &CorruptDBRequestHandler, base::SequencedTaskRunner::GetCurrentDefault(), |
| bucket_locator, s_corrupt_db_test_prefix, this)); |
| embedded_test_server()->StartAcceptingConnections(); |
| |
| std::string test_file = std::string(s_corrupt_db_test_prefix) + |
| "corrupted_open_db_detection.html#" + GetParam(); |
| SimpleTest(embedded_test_server()->GetURL(test_file)); |
| |
| test_file = |
| std::string(s_corrupt_db_test_prefix) + "corrupted_open_db_recovery.html"; |
| SimpleTest(embedded_test_server()->GetURL(test_file)); |
| } |
| #endif // !BUILDFLAG(IS_WIN) && !BUILDFLAG(IS_IOS) |
| |
| // TODO: http://crbug.com/510520, flaky on all platforms |
| IN_PROC_BROWSER_TEST_F(IndexedDBLevelDBOnlyTest, |
| DISABLED_DeleteCompactsBackingStore) { |
| const GURL kTestUrl = GetTestUrl("indexeddb", "delete_compact.html"); |
| const blink::StorageKey kTestStorageKey = |
| blink::StorageKey::CreateFirstParty(url::Origin::Create(kTestUrl)); |
| SimpleTest(GURL(kTestUrl.spec() + "#fill")); |
| |
| int64_t after_filling = RequestUsage(); |
| EXPECT_GT(after_filling, 0); |
| |
| SimpleTest(GURL(kTestUrl.spec() + "#purge")); |
| int64_t after_deleting = RequestUsage(); |
| EXPECT_LT(after_deleting, after_filling); |
| |
| // The above tests verify basic assertions - that filling writes data and |
| // deleting reduces the amount stored. |
| |
| // The below tests make assumptions about implementation specifics, such as |
| // data compression, compaction efficiency, and the maximum amount of |
| // metadata and log data remains after a deletion. It is possible that |
| // changes to the implementation may require these constants to be tweaked. |
| |
| // 1MB, as sometimes the leveldb log is compacted to .ldb files, which are |
| // compressed. |
| const int kTestFillBytes = 1 * 1024 * 1024; |
| EXPECT_GT(after_filling, kTestFillBytes); |
| |
| const int kTestCompactBytes = 300 * 1024; // 300kB |
| EXPECT_LT(after_deleting, kTestCompactBytes); |
| } |
| |
| // Saves a File that |
| // a) has been sliced (avoids the fast-copy path) |
| // b) is over 32768 bytes |
| // to IndexedDB. Regression test for crbug.com/369670458 |
| // Unfortunately this can't use SimpleTest because it requires user activation, |
| // which is provided by `ExecJs()`. |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, LargeSlicedFile) { |
| // Generate test file. |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::ScopedTempDir temp_dir; |
| base::FilePath file_path; |
| ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); |
| ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path)); |
| ASSERT_TRUE(base::WriteFile(file_path, base::RandBytesAsVector(102400))); |
| |
| // Simulate user uploading the test file. |
| std::unique_ptr<FileChooserDelegate> delegate( |
| new FileChooserDelegate(file_path, base::DoNothing())); |
| shell()->web_contents()->SetDelegate(delegate.get()); |
| ASSERT_TRUE( |
| NavigateToURL(shell(), GetTestUrl("indexeddb", "bug_369670458.html"))); |
| TestNavigationObserver same_tab_observer( |
| shell()->web_contents(), 1, MessageLoopRunner::QuitMode::IMMEDIATE, |
| /*ignore_uncommitted_navigations=*/true); |
| EXPECT_TRUE(ExecJs(shell()->web_contents(), |
| "document.getElementById('fileInput').click();")); |
| same_tab_observer.Wait(); |
| |
| // This part is copied from `SimpleTest()`. |
| std::string result = shell()->web_contents()->GetLastCommittedURL().ref(); |
| if (result != "pass") { |
| std::string js_result = EvalJs(shell(), "getLog()").ExtractString(); |
| FAIL() << "Failed: " << js_result; |
| } |
| } |
| |
| // Complex multi-step (converted from pyauto) tests begin here. |
| |
| // Verify null key path persists after restarting browser. |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, PRE_NullKeyPathPersistence) { |
| NavigateAndWaitForTitle(shell(), "bug_90635.html", "#part1", |
| "pass - first run"); |
| } |
| |
| // Verify null key path persists after restarting browser. |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, NullKeyPathPersistence) { |
| NavigateAndWaitForTitle(shell(), "bug_90635.html", "#part2", |
| "pass - second run"); |
| } |
| |
| // Disable this test on Android due to failures. See crbug.com/427529 and |
| // crbug.com/1116464 for details. |
| #if defined(ANDROID) |
| #define MAYBE_ConnectionsClosedOnTabClose DISABLED_ConnectionsClosedOnTabClose |
| #else |
| #define MAYBE_ConnectionsClosedOnTabClose ConnectionsClosedOnTabClose |
| #endif |
| // Verify that open DB connections are closed when a tab is destroyed. |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, |
| MAYBE_ConnectionsClosedOnTabClose) { |
| NavigateAndWaitForTitle(shell(), "version_change_blocked.html", "#tab1", |
| "setVersion(2) complete"); |
| |
| // Start on a different URL to force a new renderer process. |
| Shell* new_shell = CreateBrowser(); |
| EXPECT_TRUE(NavigateToURL(new_shell, GURL(url::kAboutBlankURL))); |
| NavigateAndWaitForTitle(new_shell, "version_change_blocked.html", "#tab2", |
| "setVersion(3) blocked"); |
| |
| std::u16string expected_title16(u"setVersion(3) complete"); |
| TitleWatcher title_watcher(new_shell->web_contents(), expected_title16); |
| |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()->Shutdown(0); |
| shell()->Close(); |
| |
| EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle()); |
| } |
| |
| // Verify that a "close" event is fired at database connections when |
| // the backing store is deleted. |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, ForceCloseEventTest) { |
| constexpr char kFilename[] = "force_close_event.html"; |
| NavigateAndWaitForTitle(shell(), kFilename, nullptr, "connection ready"); |
| DeleteBucketData(blink::StorageKey::CreateFirstParty( |
| url::Origin::Create(GetTestUrl("indexeddb", kFilename)))); |
| std::u16string expected_title16(u"connection closed"); |
| TitleWatcher title_watcher(shell()->web_contents(), expected_title16); |
| title_watcher.AlsoWaitForTitle(u"connection closed with error"); |
| EXPECT_EQ(expected_title16, title_watcher.WaitAndGetTitle()); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, ShutdownWithRequests) { |
| SimpleTest(GetTestUrl("indexeddb", "shutdown_with_requests.html")); |
| } |
| |
| // Regression test for https://crbug.com/429974682 |
| IN_PROC_BROWSER_TEST_P(IndexedDBBrowserTest, DeleteOpenUse) { |
| SimpleTest(GetTestUrl("indexeddb", "delete_open_use.html")); |
| } |
| |
| // Verifies that a "NotFound" DOMException is thrown on reading a large value |
| // when the underlying blob file has been deleted but the record is not. |
| // TODO(crbug.com/419264073): Adapt this test to run the second part (expecting |
| // the right error when an "external" file is missing) on SQLite too if LevelDB |
| // blob-files are migrated as-is. |
| IN_PROC_BROWSER_TEST_F(IndexedDBLevelDBOnlyTest, LargeValueReadBlobMissing) { |
| base::HistogramTester histogram_tester; |
| |
| // First write a large value that gets wrapped in a blob. |
| const GURL kTestUrl = |
| GetTestUrl("indexeddb", "write_and_read_large_value.html"); |
| SimpleTest(kTestUrl); |
| |
| // The following metric is logged in the renderer process, so force those |
| // metrics to be collected before checking `histogram_tester`. |
| content::FetchHistogramsFromChildProcesses(); |
| histogram_tester.ExpectTotalCount("IndexedDB.WrappedBlobLoadTime", 1); |
| |
| // Delete the blob file that got created. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| ASSERT_OK_AND_ASSIGN( |
| const storage::BucketInfo bucket_info, |
| GetOrCreateBucket(storage::BucketInitParams::ForDefaultBucket( |
| blink::StorageKey::CreateFirstParty( |
| url::Origin::Create(kTestUrl))))); |
| base::FilePath blob_path = |
| PathForBlob(bucket_info.ToBucketLocator(), /*database_id=*/1, |
| DatabaseMetaDataKey::kBlobNumberGeneratorInitialNumber); |
| ASSERT_TRUE(base::PathExists(blob_path)); |
| ASSERT_TRUE(base::DeleteFile(blob_path)); |
| } |
| |
| // Now attempt to read the large value again and expect an error. |
| EXPECT_THAT(EvalJs(shell(), "readData()"), |
| EvalJsResult::ErrorIs(testing::HasSubstr( |
| "NotReadableError: Data lost due to missing file. " |
| "Affected record should be considered irrecoverable"))); |
| |
| // Verify that the right set of histograms were recorded. |
| content::FetchHistogramsFromChildProcesses(); |
| const int kExpectedBucketCount = 1; |
| const int kFailureTypeBackendReadError = 3; // From file_reader_loader.h. |
| const int kFileErrorOK = 0; // From file_error.h. |
| const int kFileErrorCodeNotFoundErr = 1; // From file_error.h. |
| histogram_tester.ExpectUniqueSample( |
| "Storage.Blob.FileReaderLoader.ReadError2", -net::ERR_FILE_NOT_FOUND, |
| kExpectedBucketCount); |
| histogram_tester.ExpectUniqueSample( |
| "Storage.Blob.FileReaderLoader.FailureType2", |
| kFailureTypeBackendReadError, kExpectedBucketCount); |
| histogram_tester.GetAllSamples("IndexedDB.LargeValueReadResult"), |
| testing::ElementsAre( |
| base::Bucket(kFileErrorOK, kExpectedBucketCount), |
| base::Bucket(kFileErrorCodeNotFoundErr, kExpectedBucketCount)); |
| histogram_tester.ExpectTotalCount("IndexedDB.WrappedBlobLoadTime", 1); |
| } |
| |
| // The blob key corruption test runs in a separate class to avoid corrupting |
| // an IDB store that other tests use. |
| // This test is for https://crbug.com/1039446. |
| typedef IndexedDBLevelDBOnlyTest IndexedDBBrowserTestBlobKeyCorruption; |
| |
| // Verify the blob key corruption state recovery: |
| // - Create a file that should be the 'first' blob file. |
| // - open a database that tries to write a blob. |
| // - verify the new blob key is correct. |
| // Regression test for crbug.com/40666839 |
| IN_PROC_BROWSER_TEST_F(IndexedDBBrowserTestBlobKeyCorruption, LifecycleTest) { |
| ASSERT_TRUE(embedded_test_server()->Started() || |
| embedded_test_server()->InitializeAndListen()); |
| embedded_test_server()->RegisterRequestHandler( |
| base::BindRepeating(&StaticFileRequestHandler, s_indexeddb_test_prefix)); |
| embedded_test_server()->StartAcceptingConnections(); |
| |
| // Set up the IndexedDB instance so it contains our reference data. |
| std::string test_file = |
| std::string(s_indexeddb_test_prefix) + "write_and_read_blob.html"; |
| SimpleTest(embedded_test_server()->GetURL(test_file)); |
| |
| // Find the bucket that was created. |
| ASSERT_OK_AND_ASSIGN( |
| const auto bucket_info, |
| GetOrCreateBucket(storage::BucketInitParams::ForDefaultBucket( |
| blink::StorageKey::CreateFirstParty( |
| url::Origin::Create(embedded_test_server()->base_url()))))); |
| const auto bucket_locator = bucket_info.ToBucketLocator(); |
| |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| int blob_number = DatabaseMetaDataKey::kBlobNumberGeneratorInitialNumber; |
| base::FilePath next_blob, first_blob; |
| for (next_blob = PathForBlob(bucket_locator, 1, blob_number); |
| base::PathExists(next_blob); |
| next_blob = PathForBlob(bucket_locator, 1, blob_number++)) { |
| first_blob = next_blob; |
| } |
| |
| EXPECT_TRUE(base::PathExists(first_blob)); |
| EXPECT_FALSE(base::PathExists(next_blob)); |
| const char kCorruptData[] = "corrupt"; |
| base::WriteFile(next_blob, kCorruptData); |
| |
| std::string first_blob_contents; |
| base::ReadFileToString(first_blob, &first_blob_contents); |
| |
| // The test passes if the corrupt blob file doesn't cause a DOMException. |
| SimpleTest(embedded_test_server()->GetURL(test_file)); |
| |
| base::FilePath blob_after_corrupt_blob = |
| PathForBlob(bucket_locator, 1, blob_number); |
| EXPECT_TRUE(base::PathExists(blob_after_corrupt_blob)); |
| // The contents of the newly written blob should match the old blob. |
| std::string blob_after_corrupt_blob_contents; |
| base::ReadFileToString(blob_after_corrupt_blob, |
| &blob_after_corrupt_blob_contents); |
| EXPECT_EQ(first_blob_contents, blob_after_corrupt_blob_contents); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBIncognitoTest, BucketDurabilityStrict) { |
| DISABLED_FOR_SQLITE_PENDING_FAILURE_INJECTION(); |
| FailOperation(FailClass::LEVELDB_TRANSACTION, FailMethod::COMMIT_SYNC, 2, 1); |
| SimpleTest(GetTestUrl("indexeddb", "bucket_durability_strict.html"), shell_); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBIncognitoTest, BucketDurabilityRelaxed) { |
| DISABLED_FOR_SQLITE_PENDING_FAILURE_INJECTION(); |
| FailOperation(FailClass::LEVELDB_TRANSACTION, FailMethod::COMMIT_SYNC, 2, 1); |
| SimpleTest(GetTestUrl("indexeddb", "bucket_durability_relaxed.html"), shell_); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(IndexedDBIncognitoTest, BucketDurabilityOverride) { |
| DISABLED_FOR_SQLITE_PENDING_FAILURE_INJECTION(); |
| FailOperation(FailClass::LEVELDB_TRANSACTION, FailMethod::COMMIT_SYNC, 2, 1); |
| SimpleTest(GetTestUrl("indexeddb", "bucket_durability_override.html"), |
| shell_); |
| } |
| |
| constexpr auto GetBackingStoreTestCaseName = |
| [](const testing::TestParamInfo<bool>& info) { |
| return info.param ? "WithSqlite" : "WithLevelDb"; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| IndexedDBBrowserTest, |
| testing::Bool(), |
| GetBackingStoreTestCaseName); |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| IndexedDBBrowserTestWithLowQuota, |
| testing::Bool(), |
| GetBackingStoreTestCaseName); |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| IndexedDBBrowserTestWithGCExposed, |
| testing::Bool(), |
| GetBackingStoreTestCaseName); |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| IndexedDBIncognitoTest, |
| testing::Combine(testing::Bool(), testing::Bool()), |
| [](const testing::TestParamInfo<std::tuple<bool, bool>>& info) { |
| return base::StrCat( |
| {std::get<0>(info.param) ? "WithSqlite" : "WithLevelDb", "_", |
| std::get<1>(info.param) ? "Incognito" : "Regular"}); |
| }); |
| |
| } // namespace |
| } // namespace content::indexed_db |