blob: 2568e86e2df47306c4bd8babfdecffca87e3cc15 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/webdata/common/web_database.h"
#include <memory>
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/raw_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/webdata/common/web_database_table.h"
#include "sql/database.h"
#include "sql/init_status.h"
#include "sql/initialization.h"
#include "sql/meta_table.h"
#include "sql/test/drive_error_test_vfs.h"
#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
// Functor for closing an `sqlite3` database.
struct Sqlite3Close {
void operator()(sqlite3* ptr) const { sqlite3_close(ptr); }
};
class WebDatabaseTest : public testing::Test {
public:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
db_path_ = temp_dir_.GetPath().AppendASCII("TestWebDatabase");
}
protected:
base::test::TaskEnvironment task_environment_;
base::ScopedTempDir temp_dir_;
base::FilePath db_path_;
};
int GetVersionNumber(const base::FilePath& db_path) {
sql::Database raw_db(sql::test::kTestTag);
CHECK(raw_db.Open(db_path));
sql::InitializedMetaTable meta_table(raw_db);
return meta_table.GetVersionNumber();
}
void SetVersionNumber(const base::FilePath& db_path,
int version,
int compatible_version) {
sql::Database raw_db(sql::test::kTestTag);
CHECK(raw_db.Open(db_path));
sql::InitializedMetaTable meta_table(raw_db);
CHECK(meta_table.SetVersionNumber(version));
CHECK(meta_table.SetCompatibleVersionNumber(compatible_version));
}
TEST_F(WebDatabaseTest, InitSuccess) {
base::HistogramTester histogram_tester;
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_OK);
histogram_tester.ExpectUniqueSample("WebDatabase.InitResult",
WebDatabase::InitResult::kSuccess, 1);
}
TEST_F(WebDatabaseTest, InitPreservesCompatibleFutureVersion) {
// Create a database with a future compatible version number.
{
WebDatabase db;
ASSERT_EQ(db.Init(db_path_), sql::INIT_OK);
}
SetVersionNumber(db_path_, /*version=*/WebDatabase::kCurrentVersionNumber + 1,
/*compatible_version=*/WebDatabase::kCurrentVersionNumber);
// Init() should preserve the database.
ASSERT_EQ(GetVersionNumber(db_path_), WebDatabase::kCurrentVersionNumber + 1);
{
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_OK);
}
EXPECT_EQ(GetVersionNumber(db_path_), WebDatabase::kCurrentVersionNumber + 1);
}
TEST_F(WebDatabaseTest, InitRazesIncompatibleOldVersion) {
// Create a database with a deprecated version number.
{
WebDatabase db;
ASSERT_EQ(db.Init(db_path_), sql::INIT_OK);
}
SetVersionNumber(
db_path_, /*version=*/WebDatabase::kDeprecatedVersionNumber,
/*compatible_version=*/WebDatabase::kDeprecatedVersionNumber);
// Init() should raze the incompatible old database and initialize a new one
// successfully.
ASSERT_EQ(GetVersionNumber(db_path_), WebDatabase::kDeprecatedVersionNumber);
{
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_OK);
}
EXPECT_EQ(GetVersionNumber(db_path_), WebDatabase::kCurrentVersionNumber);
}
TEST_F(WebDatabaseTest, InitRazesIncompatibleFutureVersion) {
// Create a database with a compatible version number newer than
// kCurrentVersionNumber.
{
WebDatabase db;
ASSERT_EQ(db.Init(db_path_), sql::INIT_OK);
}
SetVersionNumber(
db_path_, /*version=*/WebDatabase::kCurrentVersionNumber + 1,
/*compatible_version=*/WebDatabase::kCurrentVersionNumber + 1);
// Init() should raze the incompatible future database and initialize a new
// one successfully.
ASSERT_EQ(GetVersionNumber(db_path_), WebDatabase::kCurrentVersionNumber + 1);
{
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_OK);
}
ASSERT_EQ(GetVersionNumber(db_path_), WebDatabase::kCurrentVersionNumber);
}
TEST_F(WebDatabaseTest, InitFailureCouldNotOpenIfWriteLockHeld) {
// Create a raw sqlite3 database connection holding a writer lock.
sql::EnsureSqliteInitialized();
std::unique_ptr<sqlite3, Sqlite3Close> raw_sqlite_db;
ASSERT_EQ(sqlite3_open(db_path_.AsUTF8Unsafe().c_str(),
std::out_ptr(raw_sqlite_db)),
SQLITE_OK);
ASSERT_EQ(sqlite3_exec(raw_sqlite_db.get(), "BEGIN EXCLUSIVE;", nullptr,
nullptr, nullptr),
SQLITE_OK);
// Other database connection will fail to acquire the lock.
base::HistogramTester histogram_tester;
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult", WebDatabase::InitResult::kCouldNotOpen, 1);
}
TEST_F(WebDatabaseTest, InitFailureCommitFailsIfReadLockHeld) {
// Create a raw sqlite3 database connection holding a read lock.
sql::EnsureSqliteInitialized();
std::unique_ptr<sqlite3, Sqlite3Close> raw_sqlite_db;
ASSERT_EQ(sqlite3_open(db_path_.AsUTF8Unsafe().c_str(),
std::out_ptr(raw_sqlite_db)),
SQLITE_OK);
ASSERT_EQ(
sqlite3_exec(raw_sqlite_db.get(), "BEGIN;", nullptr, nullptr, nullptr),
SQLITE_OK);
ASSERT_EQ(
sqlite3_exec(raw_sqlite_db.get(), "SELECT COUNT(*) FROM sqlite_schema;",
nullptr, nullptr, nullptr),
SQLITE_OK);
// Other database connection will fail to acquire the lock.
base::HistogramTester histogram_tester;
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult",
WebDatabase::InitResult::kFailedToCommitInitTransaction, 1);
}
TEST_F(WebDatabaseTest, InitSharesReadLockIfDbExists) {
// Create a table on disk.
{
WebDatabase db;
ASSERT_EQ(db.Init(db_path_), sql::INIT_OK);
}
// Create a raw sqlite3 database connection holding a read lock.
std::unique_ptr<sqlite3, Sqlite3Close> raw_sqlite_db;
ASSERT_EQ(sqlite3_open(db_path_.AsUTF8Unsafe().c_str(),
std::out_ptr(raw_sqlite_db)),
SQLITE_OK);
ASSERT_EQ(
sqlite3_exec(raw_sqlite_db.get(), "SELECT COUNT(*) FROM sqlite_schema;",
nullptr, nullptr, nullptr),
SQLITE_OK);
// `WebDatabase` connection will share the read lock.
base::HistogramTester histogram_tester;
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_OK);
// Writing fails because write lock can't be acquired.
EXPECT_FALSE(db.GetSQLConnection()->Execute("BEGIN EXCLUSIVE"));
// `WebDatabase` can grab the write lock if it becomes available.
raw_sqlite_db.reset();
EXPECT_TRUE(db.GetSQLConnection()->Execute("BEGIN EXCLUSIVE"));
}
TEST_F(WebDatabaseTest, InitFailureDatabaseLockedIfDriveIsFull) {
sql::test::DriveErrorTestVfs vfs;
base::HistogramTester histogram_tester;
WebDatabase db;
// When the drive is full, the `sql::Database` can still be opened, but
// nothing can be written.
vfs.set_drive_full(true);
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
vfs.set_drive_full(false);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult", WebDatabase::InitResult::kMetaTableInitFailed,
1);
}
TEST_F(WebDatabaseTest, InitFailureCouldNotRazeIncompatibleVersion) {
sql::test::DriveErrorTestVfs vfs;
// Create a database with a deprecated version number.
{
WebDatabase db;
ASSERT_EQ(db.Init(db_path_), sql::INIT_OK);
}
SetVersionNumber(
db_path_, /*version=*/WebDatabase::kDeprecatedVersionNumber,
/*compatible_version=*/WebDatabase::kDeprecatedVersionNumber);
base::HistogramTester histogram_tester;
WebDatabase db;
// Force disk writes to fail so RazeIfIncompatible cannot raze the file.
vfs.set_drive_full(true);
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
vfs.set_drive_full(false);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult",
WebDatabase::InitResult::kCouldNotRazeIncompatibleVersion, 1);
}
TEST_F(WebDatabaseTest, InitFailureMetaTableInitFailed) {
// Create an incompatible 'meta' index, causing the 'meta' table creation to
// fail.
{
sql::Database raw_db(sql::test::kTestTag);
ASSERT_TRUE(raw_db.Open(db_path_));
ASSERT_TRUE(raw_db.Execute("CREATE TABLE foo (id INTEGER)"));
ASSERT_TRUE(raw_db.Execute("CREATE INDEX meta ON foo(id)"));
}
base::HistogramTester histogram_tester;
WebDatabase db;
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult", WebDatabase::InitResult::kMetaTableInitFailed,
1);
}
// A fake `WebDatabaseTable` that always fails upgrading to newer versions.
class MigrationFailedTable : public WebDatabaseTable {
public:
WebDatabaseTable::TypeKey GetTypeKey() const override {
static int table_key = 0;
return reinterpret_cast<void*>(&table_key);
}
bool CreateTablesIfNecessary() override { return true; }
bool MigrateToVersion(int version, bool* update_compatible_version) override {
return false;
}
};
TEST_F(WebDatabaseTest, InitFailureMigrationError) {
base::HistogramTester histogram_tester;
// Create a database at an older version that requires migration.
{
sql::Database raw_db(sql::test::kTestTag);
ASSERT_TRUE(raw_db.Open(db_path_));
sql::MetaTable meta_table;
ASSERT_TRUE(meta_table.Init(&raw_db,
WebDatabase::kDeprecatedVersionNumber + 1,
WebDatabase::kDeprecatedVersionNumber + 1));
}
MigrationFailedTable table;
WebDatabase db;
db.AddTable(&table);
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult", WebDatabase::InitResult::kMigrationError, 1);
}
// A fake `WebDatabaseTable` that always fails creating tables.
class CreateTableFailedTable : public WebDatabaseTable {
public:
WebDatabaseTable::TypeKey GetTypeKey() const override {
static int table_key = 0;
return reinterpret_cast<void*>(&table_key);
}
bool CreateTablesIfNecessary() override { return false; }
bool MigrateToVersion(int version, bool* update_compatible_version) override {
return true;
}
};
TEST_F(WebDatabaseTest, InitFailureFailedToCreateTable) {
base::HistogramTester histogram_tester;
CreateTableFailedTable table;
WebDatabase db;
db.AddTable(&table);
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult", WebDatabase::InitResult::kFailedToCreateTable,
1);
}
// A fake `WebDatabaseTable` that causes the `sql::Transaction` in
// `WebDatabase::Init()` to fail.
class CommitFailedTable : public WebDatabaseTable {
public:
explicit CommitFailedTable(sql::test::DriveErrorTestVfs* vfs) : vfs_(vfs) {}
WebDatabaseTable::TypeKey GetTypeKey() const override {
static int table_key = 0;
return reinterpret_cast<void*>(&table_key);
}
bool CreateTablesIfNecessary() override {
vfs_->set_drive_unusable(true);
return true;
}
bool MigrateToVersion(int version, bool* update_compatible_version) override {
return true;
}
private:
raw_ptr<sql::test::DriveErrorTestVfs> vfs_;
};
TEST_F(WebDatabaseTest, InitFailureFailedToCommitInitTransaction) {
base::HistogramTester histogram_tester;
sql::test::DriveErrorTestVfs vfs;
CommitFailedTable table(&vfs);
WebDatabase db;
db.AddTable(&table);
EXPECT_EQ(db.Init(db_path_), sql::INIT_FAILURE);
vfs.set_drive_unusable(false);
histogram_tester.ExpectUniqueSample(
"WebDatabase.InitResult",
WebDatabase::InitResult::kFailedToCommitInitTransaction, 1);
}
} // namespace