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