blob: 3b400bece5f58583fd0ea882af2b5a8ae714454d [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/offline_pages/core/offline_page_metadata_store.h"
#include <stdint.h>
#include <memory>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/ref_counted.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind_test_util.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/model/offline_page_item_generator.h"
#include "components/offline_pages/core/offline_clock.h"
#include "components/offline_pages/core/offline_page_item.h"
#include "components/offline_pages/core/offline_page_metadata_store.h"
#include "components/offline_pages/core/offline_page_model.h"
#include "components/offline_pages/core/offline_page_thumbnail.h"
#include "components/offline_pages/core/offline_store_utils.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace offline_pages {
namespace {
#define OFFLINE_PAGES_TABLE_V1 "offlinepages_v1"
const char kTestClientNamespace[] = "CLIENT_NAMESPACE";
const char kTestURL[] = "https://example.com";
const char kOriginalTestURL[] = "https://example.com/foo";
const ClientId kTestClientId1(kTestClientNamespace, "1234");
const ClientId kTestClientId2(kTestClientNamespace, "5678");
const base::FilePath::CharType kFilePath[] =
FILE_PATH_LITERAL("/offline_pages/example_com.mhtml");
int64_t kFileSize = 234567LL;
int64_t kOfflineId = 12345LL;
const char kTestRequestOrigin[] = "request.origin";
int64_t kTestSystemDownloadId = 42LL;
const char kTestDigest[] = "test-digest";
const base::Time kThumbnailExpiration = store_utils::FromDatabaseTime(42);
// Build a store with outdated schema to simulate the upgrading process.
void BuildTestStoreWithSchemaFromM52(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL, "
"creation_time INTEGER NOT NULL, "
"file_size INTEGER NOT NULL, "
"version INTEGER NOT NULL, "
"last_access_time INTEGER NOT NULL, "
"access_count INTEGER NOT NULL, "
"status INTEGER NOT NULL DEFAULT 0, "
"user_initiated INTEGER, "
"client_namespace VARCHAR NOT NULL, "
"client_id VARCHAR NOT NULL, "
"online_url VARCHAR NOT NULL, "
"offline_url VARCHAR NOT NULL DEFAULT '', "
"file_path VARCHAR NOT NULL "
")"));
ASSERT_TRUE(connection.CommitTransaction());
sql::Statement statement(connection.GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, version, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, kOfflineId);
statement.BindInt(1, 0);
statement.BindInt64(2, kFileSize);
statement.BindInt(3, 0);
statement.BindInt(4, 0);
statement.BindInt(5, 1);
statement.BindCString(6, kTestClientNamespace);
statement.BindString(7, kTestClientId2.id);
statement.BindCString(8, kTestURL);
statement.BindString(9, base::FilePath(kFilePath).MaybeAsASCII());
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
ASSERT_FALSE(
connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "expiration_time"));
}
void BuildTestStoreWithSchemaFromM53(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL, "
"creation_time INTEGER NOT NULL, "
"file_size INTEGER NOT NULL, "
"version INTEGER NOT NULL, "
"last_access_time INTEGER NOT NULL, "
"access_count INTEGER NOT NULL, "
"status INTEGER NOT NULL DEFAULT 0, "
"user_initiated INTEGER, "
"expiration_time INTEGER NOT NULL DEFAULT 0, "
"client_namespace VARCHAR NOT NULL, "
"client_id VARCHAR NOT NULL, "
"online_url VARCHAR NOT NULL, "
"offline_url VARCHAR NOT NULL DEFAULT '', "
"file_path VARCHAR NOT NULL "
")"));
ASSERT_TRUE(connection.CommitTransaction());
sql::Statement statement(connection.GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, version, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path, expiration_time) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, kOfflineId);
statement.BindInt(1, 0);
statement.BindInt64(2, kFileSize);
statement.BindInt(3, 0);
statement.BindInt(4, 0);
statement.BindInt(5, 1);
statement.BindCString(6, kTestClientNamespace);
statement.BindString(7, kTestClientId2.id);
statement.BindCString(8, kTestURL);
statement.BindString(9, base::FilePath(kFilePath).MaybeAsASCII());
statement.BindInt64(10, store_utils::ToDatabaseTime(OfflineTimeNow()));
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
ASSERT_FALSE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "title"));
}
void BuildTestStoreWithSchemaFromM54(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL, "
"creation_time INTEGER NOT NULL, "
"file_size INTEGER NOT NULL, "
"version INTEGER NOT NULL, "
"last_access_time INTEGER NOT NULL, "
"access_count INTEGER NOT NULL, "
"status INTEGER NOT NULL DEFAULT 0, "
"user_initiated INTEGER, "
"expiration_time INTEGER NOT NULL DEFAULT 0, "
"client_namespace VARCHAR NOT NULL, "
"client_id VARCHAR NOT NULL, "
"online_url VARCHAR NOT NULL, "
"offline_url VARCHAR NOT NULL DEFAULT '', "
"file_path VARCHAR NOT NULL, "
"title VARCHAR NOT NULL DEFAULT ''"
")"));
ASSERT_TRUE(connection.CommitTransaction());
sql::Statement statement(connection.GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, version, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path, expiration_time, title) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, kOfflineId);
statement.BindInt(1, 0);
statement.BindInt64(2, kFileSize);
statement.BindInt(3, 0);
statement.BindInt(4, 0);
statement.BindInt(5, 1);
statement.BindCString(6, kTestClientNamespace);
statement.BindString(7, kTestClientId2.id);
statement.BindCString(8, kTestURL);
statement.BindString(9, base::FilePath(kFilePath).MaybeAsASCII());
statement.BindInt64(10, store_utils::ToDatabaseTime(OfflineTimeNow()));
statement.BindString16(11, base::UTF8ToUTF16("Test title"));
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
ASSERT_TRUE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "version"));
ASSERT_TRUE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "status"));
ASSERT_TRUE(
connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "user_initiated"));
ASSERT_TRUE(
connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "offline_url"));
}
void BuildTestStoreWithSchemaFromM55(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL, "
"creation_time INTEGER NOT NULL, "
"file_size INTEGER NOT NULL, "
"last_access_time INTEGER NOT NULL, "
"access_count INTEGER NOT NULL, "
"expiration_time INTEGER NOT NULL DEFAULT 0, "
"client_namespace VARCHAR NOT NULL, "
"client_id VARCHAR NOT NULL, "
"online_url VARCHAR NOT NULL, "
"file_path VARCHAR NOT NULL, "
"title VARCHAR NOT NULL DEFAULT ''"
")"));
ASSERT_TRUE(connection.CommitTransaction());
sql::Statement statement(connection.GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path, expiration_time, title) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, kOfflineId);
statement.BindInt(1, 0);
statement.BindInt64(2, kFileSize);
statement.BindInt(3, 0);
statement.BindInt(4, 1);
statement.BindCString(5, kTestClientNamespace);
statement.BindString(6, kTestClientId2.id);
statement.BindCString(7, kTestURL);
statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
statement.BindInt64(9, store_utils::ToDatabaseTime(OfflineTimeNow()));
statement.BindString16(10, base::UTF8ToUTF16("Test title"));
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
ASSERT_TRUE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "title"));
ASSERT_FALSE(
connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "original_url"));
}
void BuildTestStoreWithSchemaFromM56(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL, "
"creation_time INTEGER NOT NULL, "
"file_size INTEGER NOT NULL, "
"last_access_time INTEGER NOT NULL, "
"access_count INTEGER NOT NULL, "
"expiration_time INTEGER NOT NULL DEFAULT 0, "
"client_namespace VARCHAR NOT NULL, "
"client_id VARCHAR NOT NULL, "
"online_url VARCHAR NOT NULL, "
"file_path VARCHAR NOT NULL, "
"title VARCHAR NOT NULL DEFAULT '', "
"original_url VARCHAR NOT NULL DEFAULT ''"
")"));
ASSERT_TRUE(connection.CommitTransaction());
sql::Statement statement(connection.GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path, expiration_time, title, original_url) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, kOfflineId);
statement.BindInt(1, 0);
statement.BindInt64(2, kFileSize);
statement.BindInt(3, 0);
statement.BindInt(4, 1);
statement.BindCString(5, kTestClientNamespace);
statement.BindString(6, kTestClientId2.id);
statement.BindCString(7, kTestURL);
statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
statement.BindInt64(9, store_utils::ToDatabaseTime(OfflineTimeNow()));
statement.BindString16(10, base::UTF8ToUTF16("Test title"));
statement.BindCString(11, kOriginalTestURL);
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
ASSERT_TRUE(
connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "expiration_time"));
}
void BuildTestStoreWithSchemaFromM57(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL,"
" creation_time INTEGER NOT NULL,"
" file_size INTEGER NOT NULL,"
" last_access_time INTEGER NOT NULL,"
" access_count INTEGER NOT NULL,"
" client_namespace VARCHAR NOT NULL,"
" client_id VARCHAR NOT NULL,"
" online_url VARCHAR NOT NULL,"
" file_path VARCHAR NOT NULL,"
" title VARCHAR NOT NULL DEFAULT '',"
" original_url VARCHAR NOT NULL DEFAULT ''"
")"));
ASSERT_TRUE(connection.CommitTransaction());
sql::Statement statement(connection.GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path, title, original_url) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, kOfflineId);
statement.BindInt(1, 0);
statement.BindInt64(2, kFileSize);
statement.BindInt(3, 0);
statement.BindInt(4, 1);
statement.BindCString(5, kTestClientNamespace);
statement.BindString(6, kTestClientId2.id);
statement.BindCString(7, kTestURL);
statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
statement.BindString16(9, base::UTF8ToUTF16("Test title"));
statement.BindCString(10, kOriginalTestURL);
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
ASSERT_FALSE(
connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "request_origin"));
}
void BuildTestStoreWithSchemaFromM61(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL,"
" creation_time INTEGER NOT NULL,"
" file_size INTEGER NOT NULL,"
" last_access_time INTEGER NOT NULL,"
" access_count INTEGER NOT NULL,"
" client_namespace VARCHAR NOT NULL,"
" client_id VARCHAR NOT NULL,"
" online_url VARCHAR NOT NULL,"
" file_path VARCHAR NOT NULL,"
" title VARCHAR NOT NULL DEFAULT '',"
" original_url VARCHAR NOT NULL DEFAULT '',"
" request_origin VARCHAR NOT NULL DEFAULT ''"
")"));
ASSERT_TRUE(connection.CommitTransaction());
sql::Statement statement(connection.GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path, title, original_url, "
"request_origin) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, kOfflineId);
statement.BindInt(1, 0);
statement.BindInt64(2, kFileSize);
statement.BindInt(3, 0);
statement.BindInt(4, 1);
statement.BindCString(5, kTestClientNamespace);
statement.BindString(6, kTestClientId2.id);
statement.BindCString(7, kTestURL);
statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
statement.BindString16(9, base::UTF8ToUTF16("Test title"));
statement.BindCString(10, kOriginalTestURL);
statement.BindString(11, kTestRequestOrigin);
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
ASSERT_FALSE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "digest"));
}
void InjectItemInM62Store(sql::Database* db, const OfflinePageItem& item) {
ASSERT_TRUE(db->BeginTransaction());
sql::Statement statement(db->GetUniqueStatement(
"INSERT INTO " OFFLINE_PAGES_TABLE_V1
"(offline_id, creation_time, file_size, "
"last_access_time, access_count, client_namespace, "
"client_id, online_url, file_path, title, original_url, "
"request_origin, system_download_id, file_missing_time, "
"upgrade_attempt, digest) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, item.offline_id);
statement.BindInt(1, store_utils::ToDatabaseTime(item.creation_time));
statement.BindInt64(2, item.file_size);
statement.BindInt(3, store_utils::ToDatabaseTime(item.last_access_time));
statement.BindInt(4, item.access_count);
statement.BindString(5, item.client_id.name_space);
statement.BindString(6, item.client_id.id);
statement.BindString(7, item.url.spec());
statement.BindString(8, store_utils::ToDatabaseFilePath(item.file_path));
statement.BindString16(9, item.title);
statement.BindString(10, item.original_url.spec());
statement.BindString(11, item.request_origin);
statement.BindInt64(12, item.system_download_id);
statement.BindInt(13, store_utils::ToDatabaseTime(item.file_missing_time));
statement.BindInt(14, item.upgrade_attempt);
statement.BindString(15, item.digest);
ASSERT_TRUE(statement.Run());
ASSERT_TRUE(db->CommitTransaction());
}
void BuildTestStoreWithSchemaFromM62(const base::FilePath& file) {
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
ASSERT_TRUE(
connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
"(offline_id INTEGER PRIMARY KEY NOT NULL,"
" creation_time INTEGER NOT NULL,"
" file_size INTEGER NOT NULL,"
" last_access_time INTEGER NOT NULL,"
" access_count INTEGER NOT NULL,"
" system_download_id INTEGER NOT NULL DEFAULT 0,"
" file_missing_time INTEGER NOT NULL DEFAULT 0,"
" upgrade_attempt INTEGER NOT NULL DEFAULT 0,"
" client_namespace VARCHAR NOT NULL,"
" client_id VARCHAR NOT NULL,"
" online_url VARCHAR NOT NULL,"
" file_path VARCHAR NOT NULL,"
" title VARCHAR NOT NULL DEFAULT '',"
" original_url VARCHAR NOT NULL DEFAULT '',"
" request_origin VARCHAR NOT NULL DEFAULT '',"
" digest VARCHAR NOT NULL DEFAULT ''"
")"));
ASSERT_TRUE(connection.CommitTransaction());
OfflinePageItemGenerator generator;
generator.SetNamespace(kTestClientNamespace);
generator.SetId(kTestClientId2.id);
generator.SetUrl(GURL(kTestURL));
generator.SetRequestOrigin(kTestRequestOrigin);
generator.SetFileSize(kFileSize);
OfflinePageItem test_item = generator.CreateItem();
test_item.offline_id = kOfflineId;
test_item.file_path = base::FilePath(kFilePath);
InjectItemInM62Store(&connection, test_item);
}
void BuildTestStoreWithSchemaVersion1(const base::FilePath& file) {
BuildTestStoreWithSchemaFromM62(file);
sql::Database connection;
ASSERT_TRUE(
connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
ASSERT_TRUE(connection.BeginTransaction());
sql::MetaTable meta_table;
ASSERT_TRUE(meta_table.Init(&connection, 1, 1));
ASSERT_TRUE(connection.CommitTransaction());
OfflinePageItemGenerator generator;
generator.SetUrl(GURL(kTestURL));
generator.SetRequestOrigin(kTestRequestOrigin);
generator.SetFileSize(kFileSize);
generator.SetNamespace(kAsyncNamespace);
InjectItemInM62Store(&connection, generator.CreateItem());
generator.SetNamespace(kDownloadNamespace);
InjectItemInM62Store(&connection, generator.CreateItem());
generator.SetNamespace(kBrowserActionsNamespace);
InjectItemInM62Store(&connection, generator.CreateItem());
generator.SetNamespace(kNTPSuggestionsNamespace);
InjectItemInM62Store(&connection, generator.CreateItem());
}
void BuildTestStoreWithSchemaVersion2(const base::FilePath& file) {
BuildTestStoreWithSchemaVersion1(file);
sql::Database db;
ASSERT_TRUE(db.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
sql::MetaTable meta_table;
ASSERT_TRUE(meta_table.Init(&db, OfflinePageMetadataStore::kCurrentVersion,
OfflinePageMetadataStore::kCompatibleVersion));
static const char kSql[] =
"CREATE TABLE page_thumbnails"
" (offline_id INTEGER PRIMARY KEY NOT NULL,"
" expiration INTEGER NOT NULL,"
" thumbnail BLOB NOT NULL"
")";
ASSERT_TRUE(db.Execute(kSql));
}
// Create an offline page item from a SQL result. Expects complete rows with
// all columns present.
OfflinePageItem MakeOfflinePageItem(sql::Statement* statement) {
int64_t id = statement->ColumnInt64(0);
base::Time creation_time =
store_utils::FromDatabaseTime(statement->ColumnInt64(1));
int64_t file_size = statement->ColumnInt64(2);
base::Time last_access_time =
store_utils::FromDatabaseTime(statement->ColumnInt64(3));
int access_count = statement->ColumnInt(4);
int64_t system_download_id = statement->ColumnInt64(5);
base::Time file_missing_time =
store_utils::FromDatabaseTime(statement->ColumnInt64(6));
int upgrade_attempt = statement->ColumnInt(7);
ClientId client_id(statement->ColumnString(8), statement->ColumnString(9));
GURL url(statement->ColumnString(10));
base::FilePath path(
store_utils::FromDatabaseFilePath(statement->ColumnString(11)));
base::string16 title = statement->ColumnString16(12);
GURL original_url(statement->ColumnString(13));
std::string request_origin = statement->ColumnString(14);
std::string digest = statement->ColumnString(15);
OfflinePageItem item(url, id, client_id, path, file_size, creation_time);
item.last_access_time = last_access_time;
item.access_count = access_count;
item.title = title;
item.original_url = original_url;
item.request_origin = request_origin;
item.system_download_id = system_download_id;
item.file_missing_time = file_missing_time;
item.upgrade_attempt = upgrade_attempt;
item.digest = digest;
return item;
}
std::vector<OfflinePageItem> GetOfflinePagesSync(sql::Database* db) {
std::vector<OfflinePageItem> result;
static const char kSql[] = "SELECT * FROM " OFFLINE_PAGES_TABLE_V1;
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
while (statement.Step())
result.push_back(MakeOfflinePageItem(&statement));
if (!statement.Succeeded()) {
result.clear();
}
return result;
}
class OfflinePageMetadataStoreTest : public testing::Test {
public:
OfflinePageMetadataStoreTest()
: task_runner_(new base::TestMockTimeTaskRunner),
task_runner_handle_(task_runner_) {
EXPECT_TRUE(temp_directory_.CreateUniqueTempDir());
}
~OfflinePageMetadataStoreTest() override{};
protected:
void TearDown() override {
// Wait for all the pieces of the store to delete itself properly.
PumpLoop();
}
std::unique_ptr<OfflinePageMetadataStore> BuildStore() {
auto store = std::make_unique<OfflinePageMetadataStore>(
base::ThreadTaskRunnerHandle::Get(), TempPath());
PumpLoop();
return store;
}
void PumpLoop() { task_runner_->RunUntilIdle(); }
void FastForwardBy(base::TimeDelta delta) {
task_runner_->FastForwardBy(delta);
}
base::TestMockTimeTaskRunner* task_runner() const {
return task_runner_.get();
}
base::FilePath TempPath() const { return temp_directory_.GetPath(); }
OfflinePageItem CheckThatStoreHasOneItem(OfflinePageMetadataStore* store) {
std::vector<OfflinePageItem> pages = GetOfflinePages(store);
EXPECT_EQ(1U, pages.size());
return pages[0];
}
void CheckThatOfflinePageCanBeSaved(
std::unique_ptr<OfflinePageMetadataStore> store) {
size_t store_size = GetOfflinePages(store.get()).size();
OfflinePageItem offline_page(GURL(kTestURL), 1234LL, kTestClientId1,
base::FilePath(kFilePath), kFileSize);
offline_page.title = base::UTF8ToUTF16("a title");
offline_page.original_url = GURL(kOriginalTestURL);
offline_page.system_download_id = kTestSystemDownloadId;
offline_page.digest = kTestDigest;
EXPECT_EQ(ItemActionStatus::SUCCESS,
AddOfflinePage(store.get(), offline_page));
// Close the store first to ensure file lock is removed.
store.reset();
store = BuildStore();
std::vector<OfflinePageItem> pages = GetOfflinePages(store.get());
ASSERT_EQ(store_size + 1, pages.size());
if (store_size > 0 && pages[0].offline_id != offline_page.offline_id) {
std::swap(pages[0], pages[1]);
}
EXPECT_EQ(offline_page, pages[0]);
}
void CheckThatPageThumbnailCanBeSaved(OfflinePageMetadataStore* store) {
OfflinePageThumbnail thumbnail;
thumbnail.offline_id = kOfflineId;
thumbnail.expiration = kThumbnailExpiration;
thumbnail.thumbnail = "content";
AddThumbnail(store, thumbnail);
std::vector<OfflinePageThumbnail> thumbnails = GetThumbnails(store);
EXPECT_EQ(1UL, thumbnails.size());
EXPECT_EQ(thumbnail, thumbnails[0]);
}
void VerifyMetaVersions() {
sql::Database connection;
ASSERT_TRUE(connection.Open(temp_directory_.GetPath().Append(
FILE_PATH_LITERAL("OfflinePages.db"))));
ASSERT_TRUE(connection.is_open());
EXPECT_TRUE(sql::MetaTable::DoesTableExist(&connection));
sql::MetaTable meta_table;
EXPECT_TRUE(meta_table.Init(&connection, 1, 1));
EXPECT_EQ(OfflinePageMetadataStore::kCurrentVersion,
meta_table.GetVersionNumber());
EXPECT_EQ(OfflinePageMetadataStore::kCompatibleVersion,
meta_table.GetCompatibleVersionNumber());
}
void LoadAndCheckStore() {
auto store = std::make_unique<OfflinePageMetadataStore>(
base::ThreadTaskRunnerHandle::Get(), TempPath());
OfflinePageItem item = CheckThatStoreHasOneItem(store.get());
CheckThatPageThumbnailCanBeSaved((OfflinePageMetadataStore*)store.get());
CheckThatOfflinePageCanBeSaved(std::move(store));
VerifyMetaVersions();
}
void LoadAndCheckStoreFromMetaVersion1AndUp() {
// At meta version 1, more items were added to the database for testing,
// which necessitates different checks.
auto store = std::make_unique<OfflinePageMetadataStore>(
base::ThreadTaskRunnerHandle::Get(), TempPath());
std::vector<OfflinePageItem> pages = GetOfflinePages(store.get());
EXPECT_EQ(5U, pages.size());
// TODO(fgorski): Use persistent namespaces from the client policy
// controller once an appropriate method is available.
std::set<std::string> upgradeable_namespaces{
kAsyncNamespace, kDownloadNamespace, kBrowserActionsNamespace,
kNTPSuggestionsNamespace};
for (const OfflinePageItem& page : pages) {
if (upgradeable_namespaces.count(page.client_id.name_space) > 0)
EXPECT_EQ(5, page.upgrade_attempt);
else
EXPECT_EQ(0, page.upgrade_attempt);
}
CheckThatPageThumbnailCanBeSaved((OfflinePageMetadataStore*)store.get());
CheckThatOfflinePageCanBeSaved(std::move(store));
VerifyMetaVersions();
}
template <typename T>
T ExecuteSync(OfflinePageMetadataStore* store,
base::OnceCallback<T(sql::Database*)> run_callback,
T default_value) {
bool called = false;
T result;
auto result_callback = base::BindLambdaForTesting([&](T async_result) {
result = std::move(async_result);
called = true;
});
store->Execute<T>(std::move(run_callback), result_callback, default_value);
PumpLoop();
EXPECT_TRUE(called);
return result;
}
void GetOfflinePagesAsync(
OfflinePageMetadataStore* store,
base::OnceCallback<void(std::vector<OfflinePageItem>)> callback) {
auto run_callback = base::BindOnce(&GetOfflinePagesSync);
store->Execute<std::vector<OfflinePageItem>>(std::move(run_callback),
std::move(callback), {});
}
std::vector<OfflinePageItem> GetOfflinePages(
OfflinePageMetadataStore* store) {
return ExecuteSync<std::vector<OfflinePageItem>>(
store, base::BindOnce(&GetOfflinePagesSync), {});
}
ItemActionStatus AddOfflinePage(OfflinePageMetadataStore* store,
const OfflinePageItem& item) {
auto result_callback = base::BindLambdaForTesting([&](sql::Database* db) {
// Using 'INSERT OR FAIL' or 'INSERT OR ABORT' in the query below
// causes debug builds to DLOG.
static const char kSql[] =
"INSERT OR IGNORE INTO " OFFLINE_PAGES_TABLE_V1
" (offline_id, online_url, client_namespace, client_id, "
"file_path, "
"file_size, creation_time, last_access_time, access_count, "
"title, original_url, request_origin, system_download_id, "
"file_missing_time, upgrade_attempt, digest)"
" VALUES "
" (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, item.offline_id);
statement.BindString(1, item.url.spec());
statement.BindString(2, item.client_id.name_space);
statement.BindString(3, item.client_id.id);
statement.BindString(4, store_utils::ToDatabaseFilePath(item.file_path));
statement.BindInt64(5, item.file_size);
statement.BindInt64(6, store_utils::ToDatabaseTime(item.creation_time));
statement.BindInt64(7,
store_utils::ToDatabaseTime(item.last_access_time));
statement.BindInt(8, item.access_count);
statement.BindString16(9, item.title);
statement.BindString(10, item.original_url.spec());
statement.BindString(11, item.request_origin);
statement.BindInt64(12, item.system_download_id);
statement.BindInt64(13,
store_utils::ToDatabaseTime(item.file_missing_time));
statement.BindInt(14, item.upgrade_attempt);
statement.BindString(15, item.digest);
if (!statement.Run())
return ItemActionStatus::STORE_ERROR;
if (db->GetLastChangeCount() == 0)
return ItemActionStatus::ALREADY_EXISTS;
return ItemActionStatus::SUCCESS;
});
return ExecuteSync<ItemActionStatus>(store, result_callback,
ItemActionStatus::SUCCESS);
}
std::vector<OfflinePageThumbnail> GetThumbnails(
OfflinePageMetadataStore* store) {
std::vector<OfflinePageThumbnail> thumbnails;
auto run_callback = base::BindLambdaForTesting([&](sql::Database* db) {
static const char kSql[] = "SELECT * FROM page_thumbnails";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
while (statement.Step()) {
OfflinePageThumbnail thumb;
thumb.offline_id = statement.ColumnInt64(0);
thumb.expiration =
store_utils::FromDatabaseTime(statement.ColumnInt64(1));
statement.ColumnBlobAsString(2, &thumb.thumbnail);
thumbnails.push_back(std::move(thumb));
}
EXPECT_TRUE(statement.Succeeded());
return thumbnails;
});
return ExecuteSync<std::vector<OfflinePageThumbnail>>(store, run_callback,
{});
}
void AddThumbnail(OfflinePageMetadataStore* store,
const OfflinePageThumbnail& thumbnail) {
std::vector<OfflinePageThumbnail> thumbnails;
auto run_callback = base::BindLambdaForTesting([&](sql::Database* db) {
static const char kSql[] =
"INSERT INTO page_thumbnails"
" (offline_id, expiration, thumbnail) VALUES (?, ?, ?)";
sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, thumbnail.offline_id);
statement.BindInt64(1, store_utils::ToDatabaseTime(thumbnail.expiration));
statement.BindString(2, thumbnail.thumbnail);
EXPECT_TRUE(statement.Run());
return thumbnails;
});
ExecuteSync<std::vector<OfflinePageThumbnail>>(store, run_callback, {});
}
protected:
base::ScopedTempDir temp_directory_;
scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;
base::ThreadTaskRunnerHandle task_runner_handle_;
};
// Loads empty store and makes sure that there are no offline pages stored in
// it.
TEST_F(OfflinePageMetadataStoreTest, LoadEmptyStore) {
std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
EXPECT_EQ(0U, GetOfflinePages(store.get()).size());
}
TEST_F(OfflinePageMetadataStoreTest, GetOfflinePagesFromInvalidStore) {
std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
// Because execute method is self-healing this part of the test expects a
// positive results now.
store->SetStateForTesting(StoreState::NOT_LOADED, false);
EXPECT_EQ(0UL, GetOfflinePages(store.get()).size());
EXPECT_EQ(StoreState::LOADED, store->GetStateForTesting());
store->SetStateForTesting(StoreState::FAILED_LOADING, false);
EXPECT_EQ(0UL, GetOfflinePages(store.get()).size());
EXPECT_EQ(StoreState::FAILED_LOADING, store->GetStateForTesting());
store->SetStateForTesting(StoreState::FAILED_RESET, false);
EXPECT_EQ(0UL, GetOfflinePages(store.get()).size());
EXPECT_EQ(StoreState::FAILED_RESET, store->GetStateForTesting());
store->SetStateForTesting(StoreState::LOADED, true);
EXPECT_EQ(0UL, GetOfflinePages(store.get()).size());
store->SetStateForTesting(StoreState::NOT_LOADED, true);
EXPECT_EQ(0UL, GetOfflinePages(store.get()).size());
store->SetStateForTesting(StoreState::FAILED_LOADING, false);
EXPECT_EQ(0UL, GetOfflinePages(store.get()).size());
store->SetStateForTesting(StoreState::FAILED_RESET, false);
EXPECT_EQ(0UL, GetOfflinePages(store.get()).size());
}
// Loads a store which has an outdated schema.
// These tests would crash if it's not handling correctly when we're loading
// old version stores.
TEST_F(OfflinePageMetadataStoreTest, LoadVersion52Store) {
BuildTestStoreWithSchemaFromM52(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadVersion53Store) {
BuildTestStoreWithSchemaFromM53(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadVersion54Store) {
BuildTestStoreWithSchemaFromM54(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadVersion55Store) {
BuildTestStoreWithSchemaFromM55(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadVersion56Store) {
BuildTestStoreWithSchemaFromM56(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadVersion57Store) {
BuildTestStoreWithSchemaFromM57(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadVersion61Store) {
BuildTestStoreWithSchemaFromM61(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadVersion62Store) {
BuildTestStoreWithSchemaFromM62(TempPath());
LoadAndCheckStore();
}
TEST_F(OfflinePageMetadataStoreTest, LoadStoreWithMetaVersion1) {
BuildTestStoreWithSchemaVersion1(TempPath());
LoadAndCheckStoreFromMetaVersion1AndUp();
}
TEST_F(OfflinePageMetadataStoreTest, LoadStoreWithMetaVersion2) {
BuildTestStoreWithSchemaVersion2(TempPath());
LoadAndCheckStoreFromMetaVersion1AndUp();
}
// Adds metadata of an offline page into a store and then opens the store
// again to make sure that stored metadata survives store restarts.
TEST_F(OfflinePageMetadataStoreTest, AddOfflinePage) {
CheckThatOfflinePageCanBeSaved(BuildStore());
}
TEST_F(OfflinePageMetadataStoreTest, AddSameOfflinePageTwice) {
std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
OfflinePageItem offline_page(GURL(kTestURL), 1234LL, kTestClientId1,
base::FilePath(kFilePath), kFileSize);
offline_page.title = base::UTF8ToUTF16("a title");
EXPECT_EQ(ItemActionStatus::SUCCESS,
AddOfflinePage(store.get(), offline_page));
EXPECT_EQ(ItemActionStatus::ALREADY_EXISTS,
AddOfflinePage(store.get(), offline_page));
}
// Adds metadata of multiple offline pages into a store and removes some.
TEST_F(OfflinePageMetadataStoreTest, AddRemoveMultipleOfflinePages) {
std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
// Add an offline page.
OfflinePageItem offline_page_1(GURL(kTestURL), 12345LL, kTestClientId1,
base::FilePath(kFilePath), kFileSize);
EXPECT_EQ(ItemActionStatus::SUCCESS,
AddOfflinePage(store.get(), offline_page_1));
// Add anther offline page.
base::FilePath file_path_2 =
base::FilePath(FILE_PATH_LITERAL("//other.page.com.mhtml"));
OfflinePageItem offline_page_2(GURL("https://other.page.com"), 5678LL,
kTestClientId2, file_path_2, 12345,
OfflineTimeNow(), kTestRequestOrigin);
offline_page_2.original_url = GURL("https://example.com/bar");
offline_page_2.system_download_id = kTestSystemDownloadId;
offline_page_2.digest = kTestDigest;
EXPECT_EQ(ItemActionStatus::SUCCESS,
AddOfflinePage(store.get(), offline_page_2));
// Get all pages from the store.
std::vector<OfflinePageItem> pages = GetOfflinePages(store.get());
EXPECT_EQ(2U, pages.size());
// Close and reload the store.
store.reset();
store = BuildStore();
pages = GetOfflinePages(store.get());
ASSERT_EQ(2U, pages.size());
EXPECT_EQ(offline_page_2, pages[0]);
}
TEST_F(OfflinePageMetadataStoreTest, StoreCloses) {
std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
GetOfflinePages(store.get());
EXPECT_TRUE(task_runner()->HasPendingTask());
EXPECT_LT(base::TimeDelta(), task_runner()->NextPendingTaskDelay());
FastForwardBy(OfflinePageMetadataStore::kClosingDelay);
PumpLoop();
EXPECT_EQ(StoreState::NOT_LOADED, store->GetStateForTesting());
// Ensure that next call to the store will actually reinitialize it.
EXPECT_EQ(0U, GetOfflinePages(store.get()).size());
EXPECT_EQ(StoreState::LOADED, store->GetStateForTesting());
}
TEST_F(OfflinePageMetadataStoreTest, MultiplePendingCalls) {
auto store = std::make_unique<OfflinePageMetadataStore>(
base::ThreadTaskRunnerHandle::Get(), TempPath());
EXPECT_FALSE(task_runner()->HasPendingTask());
EXPECT_EQ(StoreState::NOT_LOADED, store->GetStateForTesting());
// First call flips the state to initializing.
// Subsequent calls should be pending until store is initialized.
int callback_count = 0;
auto get_complete =
base::BindLambdaForTesting([&](std::vector<OfflinePageItem> pages) {
++callback_count;
EXPECT_TRUE(pages.empty());
});
GetOfflinePagesAsync(store.get(), get_complete);
EXPECT_EQ(StoreState::INITIALIZING, store->GetStateForTesting());
GetOfflinePagesAsync(store.get(), get_complete);
EXPECT_EQ(0U, GetOfflinePages(store.get()).size());
EXPECT_EQ(StoreState::LOADED, store->GetStateForTesting());
EXPECT_EQ(2, callback_count);
}
} // namespace
} // namespace offline_pages