blob: 2f36acf7fc02c07e0fef02ec7d2997fa4b6c738e [file] [log] [blame]
// Copyright 2025 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/instance/sqlite/database_connection.h"
#include <memory>
#include <string>
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "content/browser/indexed_db/file_path_util.h"
#include "content/browser/indexed_db/indexed_db_data_loss_info.h"
#include "content/browser/indexed_db/indexed_db_value.h"
#include "content/browser/indexed_db/instance/sqlite/backing_store_impl.h"
#include "content/browser/indexed_db/status.h"
#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom-data-view.h"
namespace content::indexed_db::sqlite {
class MockBlobStorageContext : public ::storage::mojom::BlobStorageContext {
public:
MockBlobStorageContext() = default;
~MockBlobStorageContext() override = default;
void RegisterFromDataItem(mojo::PendingReceiver<::blink::mojom::Blob> blob,
const std::string& uuid,
storage::mojom::BlobDataItemPtr item) override {
NOTREACHED();
}
void RegisterFromMemory(mojo::PendingReceiver<::blink::mojom::Blob> blob,
const std::string& uuid,
::mojo_base::BigBuffer data) override {
NOTREACHED();
}
void WriteBlobToFile(mojo::PendingRemote<::blink::mojom::Blob> blob,
const base::FilePath& path,
bool flush_on_write,
std::optional<base::Time> last_modified,
WriteBlobToFileCallback callback) override {
NOTREACHED();
}
void Clone(mojo::PendingReceiver<::storage::mojom::BlobStorageContext>
receiver) override {
NOTREACHED();
}
};
class DatabaseConnectionTest : public testing::Test {
public:
DatabaseConnectionTest() = default;
~DatabaseConnectionTest() override = default;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
// Create a mock backing store for testing
auto rv =
BackingStoreImpl::OpenAndVerify(temp_dir_.GetPath(), blob_context_);
backing_store_ = std::move(std::get<0>(rv));
}
void TearDown() override { backing_store_.reset(); }
DatabaseConnection* GetDatabaseConnection(const std::u16string& name) {
auto it = backing_store()->open_connections_.find(name);
return it == backing_store()->open_connections_.end() ? nullptr
: it->second.get();
}
protected:
BackingStoreImpl* backing_store() {
return reinterpret_cast<BackingStoreImpl*>(backing_store_.get());
}
std::unique_ptr<BackingStore::Database> OpenDb(const std::u16string& name) {
StatusOr<std::unique_ptr<BackingStore::Database>> db =
backing_store()->CreateOrOpenDatabase(name);
EXPECT_TRUE(db.has_value());
EXPECT_TRUE(db.value().get());
return std::move(db.value());
}
base::test::TaskEnvironment task_environment_;
MockBlobStorageContext blob_context_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<BackingStore> backing_store_;
};
// TODO(crbug.com/419272072): de-dupe with backing_store_unittest.cc
BlobWriteCallback CreateBlobWriteCallback(
bool* succeeded,
base::OnceClosure on_done = base::DoNothing()) {
*succeeded = false;
return base::BindOnce(
[](bool* succeeded, base::OnceClosure on_done, BlobWriteResult result,
storage::mojom::WriteBlobToFileResult error) {
switch (result) {
case BlobWriteResult::kFailure:
NOTREACHED();
case BlobWriteResult::kRunPhaseTwoAsync:
case BlobWriteResult::kRunPhaseTwoAndReturnResult:
CHECK_EQ(error, storage::mojom::WriteBlobToFileResult::kSuccess);
*succeeded = true;
break;
}
std::move(on_done).Run();
return Status::OK();
},
succeeded, std::move(on_done));
}
class DatabaseConnectionCorruptionTest : public DatabaseConnectionTest {
public:
static constexpr int kObjectStoreId = 42;
const blink::IndexedDBKey kKey;
DatabaseConnectionCorruptionTest() : kKey("key") {}
// Writes a record to the DB and reads it back with `read_value_callback`,
// which should normally succeed. Then corrupts the DB and tries to read
// again, which is expected to fail gracefully. Then handles the failure by
// recovering or deleting the DB. In short: the code in `read_value_callback`
// is being verified for its error reporting, and the rest of the code in this
// function is verifying DatabaseConnection's error *handling*.
void VerifyCorruptionHandling(
base::RepeatingCallback<StatusOr<IndexedDBValue>(
BackingStore::Transaction&)> read_value_callback) {
const IndexedDBValue kValue("deadbeef", {});
const std::u16string kDbName{u"test db"};
auto db = OpenDb(kDbName);
// Create an object store with one record in it.
{
auto vc = db->CreateTransaction(
blink::mojom::IDBTransactionDurability::Default,
blink::mojom::IDBTransactionMode::VersionChange);
vc->Begin({});
ASSERT_TRUE(
vc->CreateObjectStore(kObjectStoreId, u"object store name", {}, true)
.ok());
ASSERT_TRUE(vc->PutRecord(kObjectStoreId, kKey.Clone(), kValue.Clone())
.has_value());
ASSERT_TRUE(vc->SetDatabaseVersion(1).ok());
bool succeeded = false;
ASSERT_TRUE(
vc->CommitPhaseOne(CreateBlobWriteCallback(&succeeded), {}).ok());
EXPECT_TRUE(succeeded);
ASSERT_TRUE(vc->CommitPhaseTwo().ok());
}
// Make sure that reading the record works.
auto read_value = [&]() {
auto ro =
db->CreateTransaction(blink::mojom::IDBTransactionDurability::Default,
blink::mojom::IDBTransactionMode::ReadOnly);
ro->Begin({});
StatusOr<IndexedDBValue> value = read_value_callback.Run(*ro);
ro->Rollback();
return value;
};
StatusOr<IndexedDBValue> value = read_value();
ASSERT_TRUE(value.has_value());
EXPECT_EQ(value.value().bits, kValue.bits);
// Close the database and then corrupt it.
db.reset();
base::FilePath db_path =
temp_dir_.GetPath().Append(DatabaseNameToFileName(kDbName));
ASSERT_TRUE(sql::test::CorruptIndexRootPage(db_path, "records_by_key"));
// Reopen the database. The corruption isn't detected until the index is
// used, which happens when reading from the records table.
db = OpenDb(kDbName);
value = read_value();
ASSERT_FALSE(value.has_value());
EXPECT_TRUE(value.error().IsCorruption());
// Closing the database should run the recovery routine.
db.reset();
db = OpenDb(kDbName);
value = read_value();
#if BUILDFLAG(IS_FUCHSIA)
// Read "works" in that it doesn't fail, but the record doesn't exist,
// since the corrupted DB was deleted and recreated.
ASSERT_TRUE(value.has_value());
EXPECT_TRUE(value.value().empty());
#else
// Read works because the DB was recovered.
ASSERT_TRUE(value.has_value());
EXPECT_EQ(value.value().bits, kValue.bits);
#endif
}
};
TEST_F(DatabaseConnectionCorruptionTest, Get) {
VerifyCorruptionHandling(
base::BindLambdaForTesting([&](BackingStore::Transaction& ro) {
return ro.GetRecord(kObjectStoreId, kKey);
}));
}
TEST_F(DatabaseConnectionCorruptionTest, ObjectStoreCursor) {
VerifyCorruptionHandling(
base::BindLambdaForTesting([&](BackingStore::Transaction& ro) {
return ro
.OpenObjectStoreCursor(kObjectStoreId, blink::IndexedDBKeyRange(),
blink::mojom::IDBCursorDirection::Next)
.transform([](std::unique_ptr<BackingStore::Cursor> cursor)
-> IndexedDBValue {
if (!cursor) {
return {};
}
return cursor->GetValue().Clone();
});
}));
}
} // namespace content::indexed_db::sqlite