blob: 463e0939e4077bdf88c3e885a93c6df1ba35e9d6 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "storage/browser/quota/quota_database.h"
#include <stddef.h>
#include <stdint.h>
#include <algorithm>
#include <iterator>
#include <memory>
#include <set>
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/sequence_checker.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/services/storage/public/cpp/buckets/bucket_locator.h"
#include "components/services/storage/public/cpp/buckets/constants.h"
#include "components/services/storage/public/cpp/constants.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/sqlite_result_code.h"
#include "sql/sqlite_result_code_values.h"
#include "sql/statement.h"
#include "sql/test/scoped_error_expecter.h"
#include "sql/test/test_helpers.h"
#include "storage/browser/quota/quota_client_type.h"
#include "storage/browser/quota/quota_features.h"
#include "storage/browser/quota/quota_internals.mojom.h"
#include "storage/browser/quota/storage_directory_util.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/quota/quota_types.mojom-shared.h"
#include "url/gurl.h"
using ::blink::StorageKey;
namespace storage {
namespace {
// Declared to shorten the line lengths.
static const blink::mojom::StorageType kTemp =
blink::mojom::StorageType::kTemporary;
static const blink::mojom::StorageType kSync =
blink::mojom::StorageType::kSyncable;
static const blink::mojom::StorageType kStorageTemp =
blink::mojom::StorageType::kTemporary;
static const blink::mojom::StorageType kStorageSync =
blink::mojom::StorageType::kSyncable;
static constexpr char kDatabaseName[] = "QuotaManager";
bool ContainsBucket(const std::set<BucketLocator>& buckets,
const BucketInfo& target_bucket) {
auto it = buckets.find(target_bucket.ToBucketLocator());
return it != buckets.end();
}
} // namespace
// Test parameter indicates if the database should be created for incognito
// mode, if stale buckets should be evicted, and if orphan buckets should be
// evicted.
class QuotaDatabaseTest
: public testing::TestWithParam<std::tuple<bool, bool, bool>> {
public:
QuotaDatabaseTest() {
clock_ = std::make_unique<base::SimpleTestClock>();
QuotaDatabase::SetClockForTesting(clock_.get());
feature_list_.InitWithFeatureStates(
{{features::kEvictStaleQuotaStorage, evict_stale_buckets()},
{features::kEvictOrphanQuotaStorage, evict_orphan_buckets()}});
}
protected:
using BucketTableEntry = mojom::BucketTableEntry;
void SetUp() override {
clock_->SetNow(base::Time::Now());
ASSERT_TRUE(temp_directory_.CreateUniqueTempDir());
}
void TearDown() override { ASSERT_TRUE(temp_directory_.Delete()); }
bool use_in_memory_db() const { return std::get<0>(GetParam()); }
bool evict_stale_buckets() const { return std::get<1>(GetParam()); }
bool evict_orphan_buckets() const { return std::get<2>(GetParam()); }
base::SimpleTestClock* clock() { return clock_.get(); }
base::FilePath ProfilePath() { return temp_directory_.GetPath(); }
base::FilePath DbPath() {
return ProfilePath()
.Append(kWebStorageDirectory)
.AppendASCII(kDatabaseName);
}
std::unique_ptr<QuotaDatabase> CreateDatabase(bool is_incognito) {
return std::make_unique<QuotaDatabase>(is_incognito ? base::FilePath()
: ProfilePath());
}
bool EnsureOpened(QuotaDatabase* db) {
return db->EnsureOpened() == QuotaError::kNone;
}
int GetTransactionNesting(QuotaDatabase* db) {
DCHECK_CALLED_ON_VALID_SEQUENCE(db->sequence_checker_);
return db->db_->transaction_nesting();
}
template <typename EntryType>
struct EntryVerifier {
std::set<EntryType> table;
template <size_t length>
explicit EntryVerifier(const EntryType (&entries)[length]) {
for (size_t i = 0; i < length; ++i) {
table.insert(entries[i]->Clone());
}
}
bool Run(EntryType entry) {
EXPECT_EQ(1u, table.erase(std::move(entry)));
return true;
}
};
QuotaError DumpBucketTable(
QuotaDatabase* quota_database,
const QuotaDatabase::BucketTableCallback& callback) {
return quota_database->DumpBucketTable(callback);
}
template <typename Container>
void AssignBucketTable(QuotaDatabase* quota_database, Container&& entries) {
DCHECK_CALLED_ON_VALID_SEQUENCE(quota_database->sequence_checker_);
ASSERT_TRUE(quota_database->db_);
for (const auto& entry : entries) {
static constexpr char kSql[] =
// clang-format off
"INSERT INTO buckets("
"id,"
"storage_key,"
"host,"
"type,"
"name,"
"use_count,"
"last_accessed,"
"last_modified,"
"expiration,"
"quota,"
"persistent,"
"durability) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, 0, 0)";
// clang-format on
sql::Statement statement;
statement.Assign(
quota_database->db_->GetCachedStatement(SQL_FROM_HERE, kSql));
ASSERT_TRUE(statement.is_valid());
std::optional<StorageKey> storage_key =
StorageKey::Deserialize(entry->storage_key);
ASSERT_TRUE(storage_key.has_value());
statement.BindInt64(0, entry->bucket_id);
statement.BindString(1, entry->storage_key);
statement.BindString(2, std::move(storage_key).value().origin().host());
statement.BindInt(3, static_cast<int>(entry->type));
statement.BindString(4, entry->name);
statement.BindInt(5, static_cast<int>(entry->use_count));
statement.BindTime(6, entry->last_accessed);
statement.BindTime(7, entry->last_modified);
EXPECT_TRUE(statement.Run());
}
quota_database->Commit();
}
private:
base::test::SingleThreadTaskEnvironment task_environment_;
base::ScopedTempDir temp_directory_;
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<base::SimpleTestClock> clock_;
};
TEST_P(QuotaDatabaseTest, EnsureOpened) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
if (use_in_memory_db()) {
// Path should not exist for incognito mode.
ASSERT_FALSE(base::PathExists(DbPath()));
} else {
ASSERT_TRUE(base::PathExists(DbPath()));
}
}
TEST_P(QuotaDatabaseTest, UpdateOrCreateBucket) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
BucketInitParams params(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket");
ASSERT_OK_AND_ASSIGN(BucketInfo created_bucket,
db->UpdateOrCreateBucket(params, 0));
ASSERT_GT(created_bucket.id.value(), 0);
ASSERT_EQ(created_bucket.name, params.name);
ASSERT_EQ(created_bucket.storage_key, params.storage_key);
ASSERT_EQ(created_bucket.type, kTemp);
// Should return the same bucket when querying again.
ASSERT_OK_AND_ASSIGN(BucketInfo retrieved_bucket,
db->UpdateOrCreateBucket(params, 0));
ASSERT_EQ(retrieved_bucket.id, created_bucket.id);
ASSERT_EQ(retrieved_bucket.name, created_bucket.name);
ASSERT_EQ(retrieved_bucket.storage_key, created_bucket.storage_key);
ASSERT_EQ(retrieved_bucket.type, created_bucket.type);
// Test `max_bucket_count`.
BucketInitParams params2(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket2");
EXPECT_THAT(db->UpdateOrCreateBucket(params2, 1),
base::test::ErrorIs(QuotaError::kQuotaExceeded));
// It doesn't affect the update case.
ASSERT_TRUE(db->UpdateOrCreateBucket(params, 1).has_value());
}
TEST_P(QuotaDatabaseTest, UpdateBucket) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
BucketInitParams params(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket");
params.expiration = base::Time::Now() + base::Days(1);
params.persistent = true;
ASSERT_OK_AND_ASSIGN(BucketInfo created_bucket,
db->UpdateOrCreateBucket(params, 0));
EXPECT_EQ(params.expiration, created_bucket.expiration);
EXPECT_TRUE(created_bucket.persistent);
// Should update the bucket when querying again.
params.expiration = base::Time::Now() + base::Days(2);
params.persistent = false;
params.quota = 1024 * 1024 * 20; // 20 MB
params.durability = blink::mojom::BucketDurability::kStrict;
ASSERT_OK_AND_ASSIGN(BucketInfo updated_bucket,
db->UpdateOrCreateBucket(params, 0));
EXPECT_EQ(updated_bucket.id, created_bucket.id);
// Expiration is updated.
EXPECT_EQ(updated_bucket.expiration, params.expiration);
EXPECT_NE(updated_bucket.expiration, created_bucket.expiration);
// Persistence is updated.
EXPECT_EQ(updated_bucket.persistent, params.persistent);
EXPECT_NE(updated_bucket.persistent, created_bucket.persistent);
// Quota can't be updated.
EXPECT_NE(updated_bucket.quota, params.quota);
EXPECT_EQ(updated_bucket.quota, created_bucket.quota);
// Durability can't be updated.
EXPECT_NE(updated_bucket.durability, *params.durability);
EXPECT_EQ(updated_bucket.durability, created_bucket.durability);
// Query, but without explicit policies.
params.expiration = base::Time();
params.persistent.reset();
ASSERT_OK_AND_ASSIGN(BucketInfo queried_bucket,
db->UpdateOrCreateBucket(params, 0));
// Expiration and persistence are unchanged.
EXPECT_EQ(queried_bucket.expiration, updated_bucket.expiration);
EXPECT_EQ(queried_bucket.persistent, updated_bucket.persistent);
}
TEST_P(QuotaDatabaseTest, GetOrCreateBucketDeprecated) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
StorageKey storage_key =
StorageKey::CreateFromStringForTesting("http://google/");
ASSERT_OK_AND_ASSIGN(BucketInfo created_bucket,
db->GetOrCreateBucketDeprecated(
{storage_key, kDefaultBucketName}, kSync));
ASSERT_GT(created_bucket.id.value(), 0);
ASSERT_EQ(created_bucket.name, kDefaultBucketName);
ASSERT_EQ(created_bucket.storage_key, storage_key);
ASSERT_EQ(created_bucket.type, kSync);
// Should return the same bucket when querying again.
ASSERT_OK_AND_ASSIGN(BucketInfo retrieved_bucket,
db->GetOrCreateBucketDeprecated(
{storage_key, kDefaultBucketName}, kSync));
ASSERT_EQ(retrieved_bucket.id, created_bucket.id);
ASSERT_EQ(retrieved_bucket.name, created_bucket.name);
ASSERT_EQ(retrieved_bucket.storage_key, created_bucket.storage_key);
ASSERT_EQ(retrieved_bucket.type, created_bucket.type);
}
TEST_P(QuotaDatabaseTest, GetBucket) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
// Add a bucket entry into the bucket table.
StorageKey storage_key =
StorageKey::CreateFromStringForTesting("http://google/");
std::string bucket_name = "google_bucket";
ASSERT_OK_AND_ASSIGN(
BucketInfo created_bucket,
db->CreateBucketForTesting(storage_key, bucket_name, kTemp));
ASSERT_GT(created_bucket.id.value(), 0);
ASSERT_EQ(created_bucket.name, bucket_name);
ASSERT_EQ(created_bucket.storage_key, storage_key);
ASSERT_EQ(created_bucket.type, kTemp);
ASSERT_OK_AND_ASSIGN(BucketInfo queried_bucket,
db->GetBucket(storage_key, bucket_name, kTemp));
EXPECT_EQ(queried_bucket.id, created_bucket.id);
EXPECT_EQ(queried_bucket.name, created_bucket.name);
EXPECT_EQ(queried_bucket.storage_key, created_bucket.storage_key);
ASSERT_EQ(queried_bucket.type, created_bucket.type);
// Can't retrieve buckets with name mismatch.
EXPECT_THAT(db->GetBucket(storage_key, "does_not_exist", kTemp),
base::test::ErrorIs(QuotaError::kNotFound));
// Can't retrieve buckets with StorageKey mismatch.
EXPECT_THAT(
db->GetBucket(StorageKey::CreateFromStringForTesting("http://example/"),
bucket_name, kTemp),
base::test::ErrorIs(QuotaError::kNotFound));
}
TEST_P(QuotaDatabaseTest, GetBucketById) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
// Add a bucket entry into the bucket table.
StorageKey storage_key =
StorageKey::CreateFromStringForTesting("http://google/");
std::string bucket_name = "google_bucket";
ASSERT_OK_AND_ASSIGN(
BucketInfo created_bucket,
db->CreateBucketForTesting(storage_key, bucket_name, kTemp));
ASSERT_GT(created_bucket.id.value(), 0);
ASSERT_EQ(created_bucket.name, bucket_name);
ASSERT_EQ(created_bucket.storage_key, storage_key);
ASSERT_EQ(created_bucket.type, kTemp);
ASSERT_OK_AND_ASSIGN(BucketInfo queried_bucket,
db->GetBucketById(created_bucket.id));
EXPECT_EQ(queried_bucket.name, created_bucket.name);
EXPECT_EQ(queried_bucket.storage_key, created_bucket.storage_key);
ASSERT_EQ(queried_bucket.type, created_bucket.type);
constexpr BucketId kNonExistentBucketId(7777);
EXPECT_THAT(db->GetBucketById(BucketId(kNonExistentBucketId)),
base::test::ErrorIs(QuotaError::kNotFound));
}
TEST_P(QuotaDatabaseTest, GetBucketsForType) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
const StorageKey storage_key1 =
StorageKey::CreateFromStringForTesting("http://example-a/");
const StorageKey storage_key2 =
StorageKey::CreateFromStringForTesting("http://example-b/");
const StorageKey storage_key3 =
StorageKey::CreateFromStringForTesting("http://example-c/");
ASSERT_OK_AND_ASSIGN(
BucketInfo temp_bucket1,
db->CreateBucketForTesting(storage_key1, "temp_bucket", kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo temp_bucket2,
db->CreateBucketForTesting(storage_key2, "temp_bucket", kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo sync_bucket1,
db->CreateBucketForTesting(storage_key1, kDefaultBucketName, kSync));
ASSERT_OK_AND_ASSIGN(
BucketInfo sync_bucket2,
db->CreateBucketForTesting(storage_key3, kDefaultBucketName, kSync));
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> result,
db->GetBucketsForType(kTemp));
std::set<BucketLocator> buckets = BucketInfosToBucketLocators(result);
ASSERT_EQ(2U, buckets.size());
EXPECT_TRUE(ContainsBucket(buckets, temp_bucket1));
EXPECT_TRUE(ContainsBucket(buckets, temp_bucket2));
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForType(kSync));
buckets = BucketInfosToBucketLocators(result);
ASSERT_EQ(2U, buckets.size());
EXPECT_TRUE(ContainsBucket(buckets, sync_bucket1));
EXPECT_TRUE(ContainsBucket(buckets, sync_bucket2));
}
TEST_P(QuotaDatabaseTest, GetBucketsForHost) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
ASSERT_OK_AND_ASSIGN(
BucketInfo temp_example_bucket1,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("https://example.com/"),
kDefaultBucketName, kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo temp_example_bucket2,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("http://example.com:123/"),
kDefaultBucketName, kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo perm_google_bucket1,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("http://google.com/"),
kDefaultBucketName, kSync));
ASSERT_OK_AND_ASSIGN(
BucketInfo temp_google_bucket2,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("http://google.com:123/"),
kDefaultBucketName, kTemp));
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> result,
db->GetBucketsForHost("example.com", kTemp));
ASSERT_EQ(result.size(), 2U);
EXPECT_TRUE(base::Contains(result, temp_example_bucket1));
EXPECT_TRUE(base::Contains(result, temp_example_bucket2));
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForHost("example.com", kSync));
ASSERT_EQ(result.size(), 0U);
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForHost("google.com", kSync));
ASSERT_EQ(result.size(), 1U);
EXPECT_TRUE(base::Contains(result, perm_google_bucket1));
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForHost("google.com", kTemp));
ASSERT_EQ(result.size(), 1U);
EXPECT_TRUE(base::Contains(result, temp_google_bucket2));
}
TEST_P(QuotaDatabaseTest, GetBucketsForStorageKey) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
const StorageKey storage_key1 =
StorageKey::CreateFromStringForTesting("http://example-a/");
const StorageKey storage_key2 =
StorageKey::CreateFromStringForTesting("http://example-b/");
ASSERT_OK_AND_ASSIGN(
BucketInfo temp_bucket1,
db->CreateBucketForTesting(storage_key1, "temp_test1", kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo temp_bucket2,
db->CreateBucketForTesting(storage_key1, "temp_test2", kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo sync_bucket1,
db->CreateBucketForTesting(storage_key1, kDefaultBucketName, kSync));
ASSERT_OK_AND_ASSIGN(
BucketInfo sync_bucket2,
db->CreateBucketForTesting(storage_key2, kDefaultBucketName, kSync));
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> result,
db->GetBucketsForStorageKey(storage_key1, kTemp));
std::set<BucketLocator> buckets = BucketInfosToBucketLocators(result);
ASSERT_EQ(2U, buckets.size());
EXPECT_TRUE(ContainsBucket(buckets, temp_bucket1));
EXPECT_TRUE(ContainsBucket(buckets, temp_bucket2));
ASSERT_OK_AND_ASSIGN(result,
db->GetBucketsForStorageKey(storage_key2, kSync));
buckets = BucketInfosToBucketLocators(result);
ASSERT_EQ(1U, buckets.size());
EXPECT_TRUE(ContainsBucket(buckets, sync_bucket2));
}
TEST_P(QuotaDatabaseTest, BucketLastAccessTimeLRU) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
std::set<BucketId> bucket_exceptions;
EXPECT_THAT(
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, nullptr),
base::test::ErrorIs(QuotaError::kNotFound));
// Insert bucket entries into BucketTable.
base::Time now = base::Time::Now();
using Entry = mojom::BucketTableEntryPtr;
StorageKey storage_key1 =
StorageKey::CreateFromStringForTesting("http://example-a/");
StorageKey storage_key2 =
StorageKey::CreateFromStringForTesting("http://example-b/");
StorageKey storage_key3 =
StorageKey::CreateFromStringForTesting("http://example-c/");
StorageKey storage_key4 =
StorageKey::CreateFromStringForTesting("http://example-d/");
BucketId bucket_id1 = BucketId(1);
BucketId bucket_id2 = BucketId(2);
BucketId bucket_id3 = BucketId(3);
BucketId bucket_id4 = BucketId(4);
Entry bucket1 = mojom::BucketTableEntry::New(
bucket_id1.value(), storage_key1.Serialize(), kStorageTemp,
kDefaultBucketName, -1, 99, now, now);
Entry bucket2 = mojom::BucketTableEntry::New(
bucket_id2.value(), storage_key2.Serialize(), kStorageTemp,
kDefaultBucketName, -1, 0, now, now);
Entry bucket3 =
mojom::BucketTableEntry::New(bucket_id3.value(), storage_key3.Serialize(),
kStorageTemp, "bucket_c", -1, 1, now, now);
Entry bucket4 =
mojom::BucketTableEntry::New(bucket_id4.value(), storage_key4.Serialize(),
kStorageSync, "bucket_d", -1, 5, now, now);
Entry kTableEntries[] = {bucket1->Clone(), bucket2->Clone(), bucket3->Clone(),
bucket4->Clone()};
AssignBucketTable(db.get(), kTableEntries);
// Update access time for three temporary storages, and
EXPECT_EQ(db->SetBucketLastAccessTime(
bucket_id1, base::Time::FromMillisecondsSinceUnixEpoch(10)),
QuotaError::kNone);
EXPECT_EQ(db->SetBucketLastAccessTime(
bucket_id2, base::Time::FromMillisecondsSinceUnixEpoch(20)),
QuotaError::kNone);
EXPECT_EQ(db->SetBucketLastAccessTime(
bucket_id3, base::Time::FromMillisecondsSinceUnixEpoch(30)),
QuotaError::kNone);
// One persistent.
EXPECT_EQ(db->SetBucketLastAccessTime(
bucket_id4, base::Time::FromMillisecondsSinceUnixEpoch(40)),
QuotaError::kNone);
// One non-existent.
EXPECT_EQ(db->SetBucketLastAccessTime(
BucketId(777), base::Time::FromMillisecondsSinceUnixEpoch(40)),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(
std::set<BucketLocator> result,
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, nullptr));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id1, result.begin()->id);
// Test that unlimited origins are excluded from eviction, but
// protected origins are not excluded.
auto policy = base::MakeRefCounted<MockSpecialStoragePolicy>();
policy->AddUnlimited(storage_key1.origin().GetURL());
policy->AddProtected(storage_key2.origin().GetURL());
ASSERT_OK_AND_ASSIGN(
result,
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, policy.get()));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id2, result.begin()->id);
// Test that durable origins are excluded from eviction.
policy->AddDurable(storage_key2.origin().GetURL());
ASSERT_OK_AND_ASSIGN(
result,
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, policy.get()));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id3, result.begin()->id);
// Bucket exceptions exclude specified buckets.
bucket_exceptions.insert(bucket_id1);
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForEviction(
kTemp, 1, {}, bucket_exceptions, nullptr));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id2, result.begin()->id);
bucket_exceptions.insert(bucket_id2);
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForEviction(
kTemp, 1, {}, bucket_exceptions, nullptr));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id3, result.begin()->id);
bucket_exceptions.insert(bucket_id3);
EXPECT_THAT(
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, nullptr),
base::test::ErrorIs(QuotaError::kNotFound));
EXPECT_EQ(db->SetBucketLastAccessTime(bucket_id1, base::Time::Now()),
QuotaError::kNone);
BucketLocator bucket_locator =
BucketLocator(bucket_id3, storage_key3,
static_cast<blink::mojom::StorageType>(bucket3->type),
bucket3->name == kDefaultBucketName);
// Delete storage_key/type last access time information.
ASSERT_OK_AND_ASSIGN(auto deleted, db->DeleteBucketData(bucket_locator));
EXPECT_EQ(bucket_id3, BucketId::FromUnsafeValue(deleted->bucket_id));
// Querying again to see if the deletion has worked.
bucket_exceptions.clear();
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForEviction(
kTemp, 1, {}, bucket_exceptions, nullptr));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id2, result.begin()->id);
bucket_exceptions.insert(bucket_id1);
bucket_exceptions.insert(bucket_id2);
EXPECT_THAT(
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, nullptr),
base::test::ErrorIs(QuotaError::kNotFound));
}
TEST_P(QuotaDatabaseTest, BucketPersistence) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
std::set<BucketId> bucket_exceptions;
EXPECT_THAT(
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, nullptr),
base::test::ErrorIs(QuotaError::kNotFound));
// Insert bucket entries into BucketTable.
base::Time now = base::Time::Now();
using Entry = mojom::BucketTableEntryPtr;
StorageKey storage_key1 =
StorageKey::CreateFromStringForTesting("http://example-a/");
StorageKey storage_key2 =
StorageKey::CreateFromStringForTesting("http://example-b/");
BucketId bucket_id1 = BucketId(1);
BucketId bucket_id2 = BucketId(2);
Entry bucket1 = mojom::BucketTableEntry::New(
bucket_id1.value(), storage_key1.Serialize(), kStorageTemp,
kDefaultBucketName, -1, 99, now, now);
Entry bucket2 = mojom::BucketTableEntry::New(
bucket_id2.value(), storage_key2.Serialize(), kStorageTemp,
kDefaultBucketName, -1, 0, now, now);
Entry kTableEntries[] = {bucket1->Clone(), bucket2->Clone()};
AssignBucketTable(db.get(), kTableEntries);
EXPECT_EQ(db->SetBucketLastAccessTime(
bucket_id1, base::Time::FromMillisecondsSinceUnixEpoch(10)),
QuotaError::kNone);
EXPECT_EQ(db->SetBucketLastAccessTime(
bucket_id2, base::Time::FromMillisecondsSinceUnixEpoch(20)),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(
std::set<BucketLocator> result,
db->GetBucketsForEviction(kTemp, 1, {}, bucket_exceptions, nullptr));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id1, result.begin()->id);
ASSERT_TRUE(db->UpdateBucketPersistence(bucket_id1, true).has_value());
ASSERT_OK_AND_ASSIGN(result, db->GetBucketsForEviction(
kTemp, 1, {}, bucket_exceptions, nullptr));
ASSERT_EQ(1U, result.size());
EXPECT_EQ(bucket_id2, result.begin()->id);
}
TEST_P(QuotaDatabaseTest, SetStorageKeyLastAccessTime) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
const StorageKey storage_key =
StorageKey::CreateFromStringForTesting("http://example/");
base::Time now = base::Time::Now();
// Doesn't error if bucket doesn't exist.
EXPECT_EQ(db->SetStorageKeyLastAccessTime(storage_key, kTemp, now),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(
BucketInfo bucket,
db->CreateBucketForTesting(storage_key, kDefaultBucketName, kTemp));
EXPECT_EQ(db->SetStorageKeyLastAccessTime(storage_key, kTemp, now),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(mojom::BucketTableEntryPtr info,
db->GetBucketInfoForTest(bucket.id));
EXPECT_EQ(now, info->last_accessed);
EXPECT_EQ(1, info->use_count);
}
TEST_P(QuotaDatabaseTest, GetStorageKeysForType) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
const StorageKey storage_key1 =
StorageKey::CreateFromStringForTesting("http://example-a/");
const StorageKey storage_key2 =
StorageKey::CreateFromStringForTesting("http://example-b/");
const StorageKey storage_key3 =
StorageKey::CreateFromStringForTesting("http://example-c/");
std::ignore = db->CreateBucketForTesting(storage_key1, "bucket_a", kTemp);
std::ignore = db->CreateBucketForTesting(storage_key2, "bucket_b", kTemp);
std::ignore =
db->CreateBucketForTesting(storage_key2, kDefaultBucketName, kSync);
std::ignore =
db->CreateBucketForTesting(storage_key3, kDefaultBucketName, kSync);
ASSERT_OK_AND_ASSIGN(std::set<StorageKey> result,
db->GetStorageKeysForType(kTemp));
ASSERT_TRUE(base::Contains(result, storage_key1));
ASSERT_TRUE(base::Contains(result, storage_key2));
ASSERT_FALSE(base::Contains(result, storage_key3));
ASSERT_OK_AND_ASSIGN(result, db->GetStorageKeysForType(kSync));
ASSERT_FALSE(base::Contains(result, storage_key1));
ASSERT_TRUE(base::Contains(result, storage_key2));
ASSERT_TRUE(base::Contains(result, storage_key3));
}
TEST_P(QuotaDatabaseTest, BucketLastModifiedBetween) {
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
ASSERT_OK_AND_ASSIGN(
std::set<BucketLocator> buckets,
db->GetBucketsModifiedBetween(kTemp, base::Time(), base::Time::Max()));
EXPECT_TRUE(buckets.empty());
ASSERT_OK_AND_ASSIGN(
BucketInfo bucket1,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("http://example-a/"),
"bucket_a", kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo bucket2,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("http://example-b/"),
"bucket_b", kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo bucket3,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("http://example-c/"),
"bucket_c", kTemp));
ASSERT_OK_AND_ASSIGN(
BucketInfo bucket4,
db->CreateBucketForTesting(
StorageKey::CreateFromStringForTesting("http://example-d/"),
kDefaultBucketName, kSync));
// Report last modified time for the buckets.
EXPECT_EQ(db->SetBucketLastModifiedTime(
bucket1.id, base::Time::FromMillisecondsSinceUnixEpoch(0)),
QuotaError::kNone);
EXPECT_EQ(db->SetBucketLastModifiedTime(
bucket2.id, base::Time::FromMillisecondsSinceUnixEpoch(10)),
QuotaError::kNone);
EXPECT_EQ(db->SetBucketLastModifiedTime(
bucket3.id, base::Time::FromMillisecondsSinceUnixEpoch(20)),
QuotaError::kNone);
EXPECT_EQ(db->SetBucketLastModifiedTime(
bucket4.id, base::Time::FromMillisecondsSinceUnixEpoch(30)),
QuotaError::kNone);
// Non-existent bucket.
EXPECT_EQ(db->SetBucketLastModifiedTime(
BucketId(777), base::Time::FromMillisecondsSinceUnixEpoch(0)),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(buckets, db->GetBucketsModifiedBetween(
kTemp, base::Time(), base::Time::Max()));
EXPECT_EQ(3U, buckets.size());
EXPECT_TRUE(ContainsBucket(buckets, bucket1));
EXPECT_TRUE(ContainsBucket(buckets, bucket2));
EXPECT_TRUE(ContainsBucket(buckets, bucket3));
EXPECT_FALSE(ContainsBucket(buckets, bucket4));
ASSERT_OK_AND_ASSIGN(buckets,
db->GetBucketsModifiedBetween(
kTemp, base::Time::FromMillisecondsSinceUnixEpoch(5),
base::Time::Max()));
EXPECT_EQ(2U, buckets.size());
EXPECT_FALSE(ContainsBucket(buckets, bucket1));
EXPECT_TRUE(ContainsBucket(buckets, bucket2));
EXPECT_TRUE(ContainsBucket(buckets, bucket3));
EXPECT_FALSE(ContainsBucket(buckets, bucket4));
ASSERT_OK_AND_ASSIGN(
buckets, db->GetBucketsModifiedBetween(
kTemp, base::Time::FromMillisecondsSinceUnixEpoch(15),
base::Time::Max()));
EXPECT_EQ(1U, buckets.size());
EXPECT_FALSE(ContainsBucket(buckets, bucket1));
EXPECT_FALSE(ContainsBucket(buckets, bucket2));
EXPECT_TRUE(ContainsBucket(buckets, bucket3));
EXPECT_FALSE(ContainsBucket(buckets, bucket4));
ASSERT_OK_AND_ASSIGN(
buckets, db->GetBucketsModifiedBetween(
kTemp, base::Time::FromMillisecondsSinceUnixEpoch(25),
base::Time::Max()));
EXPECT_TRUE(buckets.empty());
ASSERT_OK_AND_ASSIGN(buckets,
db->GetBucketsModifiedBetween(
kTemp, base::Time::FromMillisecondsSinceUnixEpoch(5),
base::Time::FromMillisecondsSinceUnixEpoch(15)));
EXPECT_EQ(1U, buckets.size());
EXPECT_FALSE(ContainsBucket(buckets, bucket1));
EXPECT_TRUE(ContainsBucket(buckets, bucket2));
EXPECT_FALSE(ContainsBucket(buckets, bucket3));
EXPECT_FALSE(ContainsBucket(buckets, bucket4));
ASSERT_OK_AND_ASSIGN(buckets,
db->GetBucketsModifiedBetween(
kTemp, base::Time::FromMillisecondsSinceUnixEpoch(0),
base::Time::FromMillisecondsSinceUnixEpoch(20)));
EXPECT_EQ(2U, buckets.size());
EXPECT_TRUE(ContainsBucket(buckets, bucket1));
EXPECT_TRUE(ContainsBucket(buckets, bucket2));
EXPECT_FALSE(ContainsBucket(buckets, bucket3));
EXPECT_FALSE(ContainsBucket(buckets, bucket4));
ASSERT_OK_AND_ASSIGN(buckets,
db->GetBucketsModifiedBetween(
kSync, base::Time::FromMillisecondsSinceUnixEpoch(0),
base::Time::FromMillisecondsSinceUnixEpoch(35)));
EXPECT_EQ(1U, buckets.size());
EXPECT_FALSE(ContainsBucket(buckets, bucket1));
EXPECT_FALSE(ContainsBucket(buckets, bucket2));
EXPECT_FALSE(ContainsBucket(buckets, bucket3));
EXPECT_TRUE(ContainsBucket(buckets, bucket4));
}
TEST_P(QuotaDatabaseTest, RegisterInitialStorageKeyInfo) {
auto db = CreateDatabase(use_in_memory_db());
base::flat_map<blink::mojom::StorageType, std::set<StorageKey>>
storage_keys_by_type;
const StorageKey kStorageKeys[] = {
StorageKey::CreateFromStringForTesting("http://a/"),
StorageKey::CreateFromStringForTesting("http://b/"),
StorageKey::CreateFromStringForTesting("http://c/")};
storage_keys_by_type.emplace(
kTemp, std::set<StorageKey>(kStorageKeys, std::end(kStorageKeys)));
storage_keys_by_type.emplace(
kSync, std::set<StorageKey>(kStorageKeys, std::end(kStorageKeys)));
EXPECT_EQ(db->RegisterInitialStorageKeyInfo(storage_keys_by_type),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(
BucketInfo bucket_result,
db->GetBucket(StorageKey::CreateFromStringForTesting("http://a/"),
kDefaultBucketName, kTemp));
ASSERT_OK_AND_ASSIGN(mojom::BucketTableEntryPtr info,
db->GetBucketInfoForTest(bucket_result.id));
EXPECT_EQ(0, info->use_count);
EXPECT_EQ(db->SetStorageKeyLastAccessTime(
StorageKey::CreateFromStringForTesting("http://a/"), kTemp,
base::Time::FromSecondsSinceUnixEpoch(1.0)),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(info, db->GetBucketInfoForTest(bucket_result.id));
EXPECT_EQ(1, info->use_count);
EXPECT_EQ(db->RegisterInitialStorageKeyInfo(storage_keys_by_type),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(info, db->GetBucketInfoForTest(bucket_result.id));
EXPECT_EQ(1, info->use_count);
}
TEST_P(QuotaDatabaseTest, DumpBucketTable) {
base::Time now = base::Time::Now();
using Entry = mojom::BucketTableEntryPtr;
StorageKey storage_key1 =
StorageKey::CreateFromStringForTesting("http://go/");
StorageKey storage_key2 =
StorageKey::CreateFromStringForTesting("http://oo/");
StorageKey storage_key3 =
StorageKey::CreateFromStringForTesting("http://gle/");
Entry kTableEntries[] = {
mojom::BucketTableEntry::New(1, storage_key1.Serialize(), kStorageTemp,
kDefaultBucketName, -1, 2147483647, now,
now),
mojom::BucketTableEntry::New(2, storage_key2.Serialize(), kStorageTemp,
kDefaultBucketName, -1, 0, now, now),
mojom::BucketTableEntry::New(3, storage_key3.Serialize(), kStorageTemp,
kDefaultBucketName, -1, 1, now, now),
};
auto db = CreateDatabase(use_in_memory_db());
EXPECT_TRUE(EnsureOpened(db.get()));
AssignBucketTable(db.get(), kTableEntries);
using Verifier = EntryVerifier<Entry>;
Verifier verifier(kTableEntries);
EXPECT_EQ(DumpBucketTable(db.get(),
base::BindRepeating(&Verifier::Run,
base::Unretained(&verifier))),
QuotaError::kNone);
EXPECT_TRUE(verifier.table.empty());
}
TEST_P(QuotaDatabaseTest, DeleteBucketData) {
StorageKey storage_key =
StorageKey::CreateFromStringForTesting("http://google/");
std::string bucket_name = "inbox";
// Create db, create a bucket and add bucket data. Close db by leaving scope.
{
auto db = CreateDatabase(/*is_incognito=*/false);
EXPECT_TRUE(EnsureOpened(db.get()));
ASSERT_OK_AND_ASSIGN(
BucketInfo result,
db->CreateBucketForTesting(storage_key, bucket_name, kTemp));
BucketLocator bucket = result.ToBucketLocator();
const base::FilePath idb_bucket_path = CreateClientBucketPath(
ProfilePath(), bucket, QuotaClientType::kIndexedDatabase);
ASSERT_TRUE(base::CreateDirectory(idb_bucket_path));
ASSERT_TRUE(base::WriteFile(idb_bucket_path.AppendASCII("FakeStorage"),
"fake_content"));
}
// Reopen db and verify that previously added bucket data is gone on deletion.
{
auto db = CreateDatabase(/*is_incognito=*/false);
EXPECT_TRUE(EnsureOpened(db.get()));
ASSERT_OK_AND_ASSIGN(BucketInfo result,
db->GetBucket(storage_key, bucket_name, kTemp));
BucketLocator bucket = result.ToBucketLocator();
const base::FilePath bucket_path = CreateBucketPath(ProfilePath(), bucket);
ASSERT_TRUE(base::PathExists(bucket_path));
ASSERT_TRUE(db->DeleteBucketData(bucket).has_value());
ASSERT_FALSE(base::PathExists(bucket_path));
}
}
// Non-parameterized tests.
TEST_P(QuotaDatabaseTest, BootstrapFlag) {
auto db = CreateDatabase(/*is_incognito=*/false);
EXPECT_FALSE(db->IsBootstrapped());
EXPECT_EQ(db->SetIsBootstrapped(true), QuotaError::kNone);
EXPECT_TRUE(db->IsBootstrapped());
EXPECT_EQ(db->SetIsBootstrapped(false), QuotaError::kNone);
EXPECT_FALSE(db->IsBootstrapped());
}
TEST_P(QuotaDatabaseTest, OpenCorruptedDatabase) {
base::HistogramTester histograms;
// Create database, force corruption and close db by leaving scope.
{
auto db = CreateDatabase(/*is_incognito=*/false);
ASSERT_TRUE(EnsureOpened(db.get()));
ASSERT_TRUE(sql::test::CorruptSizeInHeader(DbPath()));
// Add fake data into storage directory.
base::FilePath storage_path = db->GetStoragePath();
ASSERT_TRUE(base::CreateDirectory(storage_path));
ASSERT_TRUE(base::WriteFile(storage_path.AppendASCII("FakeStorage"),
"dummy_content"));
}
// Reopen database and verify schema reset on reopen.
{
sql::test::ScopedErrorExpecter expecter;
expecter.ExpectError(SQLITE_CORRUPT);
auto db = CreateDatabase(/*is_incognito=*/false);
ASSERT_TRUE(EnsureOpened(db.get()));
histograms.ExpectBucketCount("Quota.DatabaseSpecificError.Open",
sql::SqliteLoggedResultCode::kCorrupt, 1);
EXPECT_TRUE(expecter.SawExpectedErrors());
// Ensure no nested transactions after reentrant calls to EnsureOpened()
EXPECT_EQ(GetTransactionNesting(db.get()), 1);
// Ensure data is deleted.
base::FilePath storage_path = db->GetStoragePath();
EXPECT_FALSE(base::IsDirectoryEmpty(storage_path));
}
histograms.ExpectTotalCount("Quota.QuotaDatabaseReset", 1);
histograms.ExpectBucketCount("Quota.QuotaDatabaseReset",
DatabaseResetReason::kOpenDatabase, 1);
}
TEST_P(QuotaDatabaseTest, QuotaDatabasePathMigration) {
const base::FilePath kLegacyFilePath =
ProfilePath().AppendASCII(kDatabaseName);
BucketInitParams params(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket");
// Create database, add bucket and close by leaving scope.
{
auto db = CreateDatabase(/*is_incognito=*/false);
ASSERT_TRUE(db->UpdateOrCreateBucket(params, 0).has_value());
}
// Move db file paths to legacy file path for path migration test setup.
{
base::Move(DbPath(), kLegacyFilePath);
base::Move(sql::Database::JournalPath(DbPath()),
sql::Database::JournalPath(kLegacyFilePath));
}
// Reopen database, check that db is migrated to new path with bucket data.
{
auto db = CreateDatabase(/*is_incognito=*/false);
EXPECT_TRUE(
db->GetBucket(params.storage_key, params.name, kTemp).has_value());
EXPECT_FALSE(base::PathExists(kLegacyFilePath));
EXPECT_TRUE(base::PathExists(DbPath()));
}
}
// Test for crbug.com/1316581.
TEST_P(QuotaDatabaseTest, QuotaDatabasePathBadMigration) {
const base::FilePath kLegacyFilePath =
ProfilePath().AppendASCII(kDatabaseName);
BucketInitParams params(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket");
// Create database, add bucket and close by leaving scope.
{
auto db = CreateDatabase(/*is_incognito=*/false);
ASSERT_TRUE(db->UpdateOrCreateBucket(params, 0).has_value());
}
// Copy db file paths to legacy file path to mimic bad migration state.
base::CopyFile(DbPath(), kLegacyFilePath);
// Reopen database, check that db is migrated and is in a good state.
{
auto db = CreateDatabase(/*is_incognito=*/false);
EXPECT_TRUE(
db->GetBucket(params.storage_key, params.name, kTemp).has_value());
EXPECT_TRUE(base::PathExists(DbPath()));
}
}
// Test for crbug.com/1322375.
//
// base::CreateDirectory behaves differently on Mac and allows directory
// migration to succeed when we expect failure.
#if !BUILDFLAG(IS_APPLE)
TEST_P(QuotaDatabaseTest, QuotaDatabaseDirectoryMigrationError) {
const base::FilePath kLegacyFilePath =
ProfilePath().AppendASCII(kDatabaseName);
BucketInitParams google_params = BucketInitParams::ForDefaultBucket(
StorageKey::CreateFromStringForTesting("http://google/"));
BucketInitParams example_params = BucketInitParams::ForDefaultBucket(
StorageKey::CreateFromStringForTesting("http://example/"));
BucketId example_id;
// Create database, add bucket and close by leaving scope.
{
auto db = CreateDatabase(/*is_incognito=*/false);
// Create two buckets to check that ids are different after database reset.
ASSERT_TRUE(db->UpdateOrCreateBucket(google_params, 0).has_value());
ASSERT_OK_AND_ASSIGN(auto result,
db->UpdateOrCreateBucket(example_params, 0));
example_id = result.id;
}
{
// Delete database files to force a bad migration state.
base::DeleteFile(DbPath());
base::DeleteFile(sql::Database::JournalPath(DbPath()));
// Create a directory with the database file path to force directory
// migration to fail.
base::CreateDirectory(kLegacyFilePath);
}
{
// Open database to trigger migration. Migration failure forces a database
// reset.
auto db = CreateDatabase(/*is_incognito=*/false);
ASSERT_OK_AND_ASSIGN(auto result,
db->UpdateOrCreateBucket(example_params, 0));
// Validate database reset by checking that bucket id doesn't match.
EXPECT_NE(result.id, example_id);
}
}
#endif // !BUILDFLAG(IS_APPLE)
TEST_P(QuotaDatabaseTest, UpdateOrCreateBucket_CorruptedDatabase) {
base::HistogramTester histograms;
QuotaDatabase db(ProfilePath());
BucketInitParams params(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket");
int sqlite_error_code = 0;
db.SetDbErrorCallback(base::BindRepeating(
[](int* error_code_out, int error_code) { *error_code_out = error_code; },
&sqlite_error_code));
{
ASSERT_TRUE(db.UpdateOrCreateBucket(params, 0).has_value())
<< "Failed to create bucket to be used in test";
EXPECT_EQ(sqlite_error_code, static_cast<int>(sql::SqliteResultCode::kOk));
}
// Bucket lookup uses the `buckets_by_storage_key` index.
QuotaError corruption_error =
db.CorruptForTesting(base::BindOnce([](const base::FilePath& db_path) {
ASSERT_TRUE(
sql::test::CorruptIndexRootPage(db_path, "buckets_by_storage_key"));
}));
ASSERT_EQ(QuotaError::kNone, corruption_error)
<< "Failed to corrupt the database";
{
EXPECT_FALSE(db.UpdateOrCreateBucket(params, 0).has_value());
EXPECT_EQ(sqlite_error_code,
static_cast<int>(sql::SqliteResultCode::kCorrupt));
}
histograms.ExpectTotalCount("Quota.DatabaseSpecificError.GetBucket", 1);
}
TEST_P(QuotaDatabaseTest, Expiration) {
QuotaDatabase db(ProfilePath());
// Default `expiration` value.
BucketInitParams params(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket");
ASSERT_OK_AND_ASSIGN(BucketInfo result, db.UpdateOrCreateBucket(params, 0));
EXPECT_TRUE(result.expiration.is_null());
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> expired_buckets,
db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, expired_buckets.size());
// Non-default `expiration` value.
BucketInitParams params2(
StorageKey::CreateFromStringForTesting("http://example/"),
"example_bucket");
params2.expiration = base::Time::Now();
ASSERT_OK_AND_ASSIGN(result, db.UpdateOrCreateBucket(params2, 0));
EXPECT_EQ(params2.expiration, result.expiration);
// Update `expiration` value.
base::Time updated_time = base::Time::Now() + base::Days(1);
ASSERT_OK_AND_ASSIGN(result,
db.UpdateBucketExpiration(result.id, updated_time));
EXPECT_EQ(updated_time, result.expiration);
ASSERT_OK_AND_ASSIGN(expired_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, expired_buckets.size());
// Set expiration to the past.
updated_time = base::Time::Now() - base::Days(1);
ASSERT_OK_AND_ASSIGN(result,
db.UpdateBucketExpiration(result.id, updated_time));
EXPECT_EQ(updated_time, result.expiration);
ASSERT_OK_AND_ASSIGN(expired_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(1U, expired_buckets.size());
}
TEST_P(QuotaDatabaseTest, Stale) {
// Setup database with a few buckets.
QuotaDatabase db(ProfilePath());
BucketInitParams named_params(
StorageKey::CreateFromStringForTesting("http://google/"),
"google_bucket");
ASSERT_OK_AND_ASSIGN(BucketInfo named_bucket,
db.UpdateOrCreateBucket(named_params, 0));
BucketInitParams default_params(
StorageKey::CreateFromStringForTesting("http://notgoogle/"),
kDefaultBucketName);
ASSERT_OK_AND_ASSIGN(BucketInfo default_bucket,
db.UpdateOrCreateBucket(default_params, 0));
BucketInitParams persistent_params(
StorageKey::CreateFromStringForTesting("http://alsonotgoogle/"),
kDefaultBucketName);
persistent_params.persistent = true;
ASSERT_OK_AND_ASSIGN(BucketInfo persistent_bucket,
db.UpdateOrCreateBucket(persistent_params, 0));
BucketInitParams expired_params(
StorageKey::CreateFromStringForTesting("http://expired/"),
"expired_bucket");
expired_params.expiration = base::Time::Now() + base::Days(1);
ASSERT_OK_AND_ASSIGN(BucketInfo expired_bucket,
db.UpdateOrCreateBucket(expired_params, 0));
// Current accessed/modified time isn't stale.
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> stale_buckets,
db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, stale_buckets.size());
// Force expire bucket, this should always be returned.
ASSERT_OK_AND_ASSIGN(expired_bucket, db.UpdateBucketExpiration(
expired_bucket.id,
base::Time::Now() - base::Days(1)));
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(1U, stale_buckets.size());
// Old accessed with current modified time isn't stale.
EXPECT_EQ(db.SetBucketLastAccessTime(named_bucket.id,
base::Time::Now() - base::Days(401)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(1U, stale_buckets.size());
// Current accessed with old modified time isn't stale.
EXPECT_EQ(db.SetBucketLastAccessTime(named_bucket.id, base::Time::Now()),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(named_bucket.id,
base::Time::Now() - base::Days(401)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(1U, stale_buckets.size());
// Old accessed/modified time is stale, but we need to wait a minute.
EXPECT_EQ(db.SetBucketLastAccessTime(named_bucket.id,
base::Time::Now() - base::Days(401)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(1U, stale_buckets.size());
// If we wait a minute after initialization then it's returned as stale as
// long as it's our first check.
clock()->SetNow(base::Time::Now() + base::Minutes(1));
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(evict_stale_buckets() ? 2U : 1U, stale_buckets.size());
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(1u, stale_buckets.size());
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(evict_stale_buckets() ? 2U : 1U, stale_buckets.size());
// 399 days ago isn't enough to be stale.
EXPECT_EQ(db.SetBucketLastAccessTime(named_bucket.id,
base::Time::Now() - base::Days(399)),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(named_bucket.id,
base::Time::Now() - base::Days(399)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(1U, stale_buckets.size());
// But if we wait a day then it is enough at 400.
clock()->SetNow(base::Time::Now() + base::Days(1));
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(evict_stale_buckets() ? 2U : 1U, stale_buckets.size());
// A default bucket can be stale.
EXPECT_EQ(db.SetBucketLastAccessTime(default_bucket.id,
base::Time::Now() - base::Days(401)),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(default_bucket.id,
base::Time::Now() - base::Days(401)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(evict_stale_buckets() ? 3U : 1U, stale_buckets.size());
// A persistent bucket cannot be stale.
EXPECT_EQ(db.SetBucketLastAccessTime(persistent_bucket.id,
base::Time::Now() - base::Days(401)),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(persistent_bucket.id,
base::Time::Now() - base::Days(401)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(evict_stale_buckets() ? 3U : 1U, stale_buckets.size());
// Special storage policies are respected for default buckets.
auto policy = base::MakeRefCounted<MockSpecialStoragePolicy>();
policy->AddUnlimited(default_bucket.storage_key.origin().GetURL());
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(policy.get()));
EXPECT_EQ(evict_stale_buckets() ? 2U : 1U, stale_buckets.size());
policy = base::MakeRefCounted<MockSpecialStoragePolicy>();
policy->AddDurable(default_bucket.storage_key.origin().GetURL());
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(policy.get()));
EXPECT_EQ(evict_stale_buckets() ? 2U : 1U, stale_buckets.size());
// Special storage policies are not respected for named buckets.
policy = base::MakeRefCounted<MockSpecialStoragePolicy>();
policy->AddUnlimited(named_bucket.storage_key.origin().GetURL());
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(policy.get()));
EXPECT_EQ(evict_stale_buckets() ? 3U : 1U, stale_buckets.size());
policy = base::MakeRefCounted<MockSpecialStoragePolicy>();
policy->AddDurable(named_bucket.storage_key.origin().GetURL());
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(stale_buckets, db.GetExpiredBuckets(policy.get()));
EXPECT_EQ(evict_stale_buckets() ? 3U : 1U, stale_buckets.size());
}
TEST_P(QuotaDatabaseTest, Orphan) {
// Setup database and check no orphaned buckets counted.
QuotaDatabase db(ProfilePath());
clock()->SetNow(base::Time::Now() + base::Minutes(1));
{
base::HistogramTester histograms;
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> buckets,
db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, buckets.size());
EXPECT_EQ(0, histograms.GetTotalSum("Quota.OrphanBucketCount"));
}
// First party bucket doesn't qualify, even if it's old.
BucketInitParams first_party_params(
StorageKey::CreateFromStringForTesting("http://firstparty/"),
kDefaultBucketName);
ASSERT_OK_AND_ASSIGN(BucketInfo first_party_bucket,
db.UpdateOrCreateBucket(first_party_params, 0));
{
base::HistogramTester histograms;
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> buckets,
db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, buckets.size());
EXPECT_EQ(0, histograms.GetTotalSum("Quota.OrphanBucketCount"));
EXPECT_EQ(db.SetBucketLastAccessTime(first_party_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(first_party_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, buckets.size());
EXPECT_EQ(0, histograms.GetTotalSum("Quota.OrphanBucketCount"));
}
// First party nonce bucket does qualify, but only if it's old and we haven't
// already looked.
BucketInitParams first_party_nonce_params(
StorageKey::CreateWithNonce(
url::Origin::Create(GURL("http://firstpartynonce/")),
base::UnguessableToken::Create()),
kDefaultBucketName);
ASSERT_OK_AND_ASSIGN(BucketInfo first_party_nonce_bucket,
db.UpdateOrCreateBucket(first_party_nonce_params, 0));
{
base::HistogramTester histograms;
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> buckets,
db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, buckets.size());
EXPECT_EQ(0, histograms.GetTotalSum("Quota.OrphanBucketCount"));
EXPECT_EQ(db.SetBucketLastAccessTime(first_party_nonce_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(first_party_nonce_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, buckets.size());
EXPECT_EQ(0, histograms.GetTotalSum("Quota.OrphanBucketCount"));
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1U : 0U,
buckets.size());
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1 : 0,
histograms.GetTotalSum("Quota.OrphanBucketCount"));
}
// Third party bucket doesn't qualify, even if it's old.
BucketInitParams third_party_params(
StorageKey::Create(url::Origin::Create(GURL("https://thirdparty/")),
net::SchemefulSite(GURL("https://thirdparty2/")),
blink::mojom::AncestorChainBit::kCrossSite),
kDefaultBucketName);
ASSERT_OK_AND_ASSIGN(BucketInfo third_party_bucket,
db.UpdateOrCreateBucket(third_party_params, 0));
{
base::HistogramTester histograms;
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> buckets,
db.GetExpiredBuckets(nullptr));
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1U : 0U,
buckets.size());
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1 : 0,
histograms.GetTotalSum("Quota.OrphanBucketCount"));
EXPECT_EQ(db.SetBucketLastAccessTime(third_party_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(third_party_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1 : 0U,
buckets.size());
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 2 : 0,
histograms.GetTotalSum("Quota.OrphanBucketCount"));
}
// Third party nonce bucket does qualify, but only if it's old and we haven't
// already looked.
BucketInitParams third_party_nonce_params(
StorageKey::Create(
url::Origin::Create(GURL("https://thirdparty/")),
net::SchemefulSite(url::Origin::Create(GURL("http://thirdparty2/"))
.DeriveNewOpaqueOrigin()),
blink::mojom::AncestorChainBit::kCrossSite),
kDefaultBucketName);
ASSERT_OK_AND_ASSIGN(BucketInfo third_party_nonce_bucket,
db.UpdateOrCreateBucket(third_party_nonce_params, 0));
{
base::HistogramTester histograms;
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(std::set<BucketInfo> buckets,
db.GetExpiredBuckets(nullptr));
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1U : 0U,
buckets.size());
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1 : 0,
histograms.GetTotalSum("Quota.OrphanBucketCount"));
EXPECT_EQ(db.SetBucketLastAccessTime(third_party_nonce_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
EXPECT_EQ(db.SetBucketLastModifiedTime(third_party_nonce_bucket.id,
base::Time::Now() - base::Days(2)),
QuotaError::kNone);
ASSERT_OK_AND_ASSIGN(buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ(0U, buckets.size());
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 1 : 0,
histograms.GetTotalSum("Quota.OrphanBucketCount"));
db.SetAlreadyEvictedStaleStorageForTesting(false);
ASSERT_OK_AND_ASSIGN(buckets, db.GetExpiredBuckets(nullptr));
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 2U : 0U,
buckets.size());
EXPECT_EQ((evict_stale_buckets() && evict_orphan_buckets()) ? 3 : 0,
histograms.GetTotalSum("Quota.OrphanBucketCount"));
}
}
TEST_P(QuotaDatabaseTest, PersistentPolicy) {
QuotaDatabase db(ProfilePath());
const auto storage_key =
StorageKey::CreateFromStringForTesting("http://google/");
BucketInitParams default_params =
BucketInitParams::ForDefaultBucket(storage_key);
BucketInitParams non_default_params(storage_key, "inbox");
// Insert default bucket first (so it's LRU).
ASSERT_OK_AND_ASSIGN(BucketInfo result,
db.UpdateOrCreateBucket(default_params, 0));
const BucketId default_id = result.id;
// Then non default bucket.
ASSERT_OK_AND_ASSIGN(result, db.UpdateOrCreateBucket(non_default_params, 0));
const BucketId non_default_id = result.id;
EXPECT_NE(non_default_id, default_id);
// Get evictable bucket --- should be the default one.
auto policy = base::MakeRefCounted<MockSpecialStoragePolicy>();
ASSERT_OK_AND_ASSIGN(
std::set<BucketLocator> lru_result,
db.GetBucketsForEviction(kTemp, 1, {}, {}, policy.get()));
ASSERT_EQ(1U, lru_result.size());
EXPECT_EQ(default_id, lru_result.begin()->id);
// Check that durable policy applies to the default bucket but not the non
// default (non default buckets use the persist columnn in the database).
policy->AddDurable(storage_key.origin().GetURL());
ASSERT_OK_AND_ASSIGN(
lru_result, db.GetBucketsForEviction(kTemp, 1, {}, {}, policy.get()));
ASSERT_EQ(1U, lru_result.size());
EXPECT_EQ(non_default_id, lru_result.begin()->id);
}
INSTANTIATE_TEST_SUITE_P(
All,
QuotaDatabaseTest,
testing::Combine(/*use_in_memory_db=*/testing::Bool(),
/*evict_stale_buckets=*/testing::Bool(),
/*evict_orphan_buckets=*/testing::Bool()));
} // namespace storage