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