blob: cd2b967fcde5decde8b19ea6c664aa2d55c1a637 [file] [log] [blame]
// Copyright 2016 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 "content/browser/dom_storage/local_storage_context_mojo.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind_test_util.h"
#include "build/build_config.h"
#include "components/services/filesystem/public/interfaces/file_system.mojom.h"
#include "components/services/leveldb/public/cpp/util.h"
#include "content/browser/dom_storage/dom_storage_area.h"
#include "content/browser/dom_storage/dom_storage_context_impl.h"
#include "content/browser/dom_storage/dom_storage_database.h"
#include "content/browser/dom_storage/dom_storage_namespace.h"
#include "content/browser/dom_storage/dom_storage_task_runner.h"
#include "content/browser/dom_storage/test/fake_leveldb_database_error_on_write.h"
#include "content/browser/dom_storage/test/fake_leveldb_service.h"
#include "content/browser/dom_storage/test/mojo_test_with_file_service.h"
#include "content/browser/dom_storage/test/storage_area_test_util.h"
#include "content/common/dom_storage/dom_storage_types.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_usage_info.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_utils.h"
#include "content/test/fake_leveldb_database.h"
#include "mojo/public/cpp/bindings/associated_binding.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "mojo/public/cpp/bindings/binding_set.h"
#include "mojo/public/cpp/bindings/strong_associated_binding.h"
#include "services/file/public/mojom/constants.mojom.h"
#include "services/file/user_id_map.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/leveldatabase/env_chromium.h"
#include "third_party/leveldatabase/src/include/leveldb/db.h"
using leveldb::StdStringToUint8Vector;
using leveldb::Uint8VectorToStdString;
namespace content {
namespace {
using test::FakeLevelDBService;
using test::FakeLevelDBDatabaseErrorOnWrite;
// An empty namespace is the local storage namespace.
constexpr const char kLocalStorageNamespaceId[] = "";
void GetStorageUsageCallback(const base::RepeatingClosure& callback,
std::vector<StorageUsageInfo>* out_result,
std::vector<StorageUsageInfo> result) {
*out_result = std::move(result);
callback.Run();
}
class TestLevelDBObserver : public blink::mojom::StorageAreaObserver {
public:
struct Observation {
enum { kAdd, kChange, kDelete, kDeleteAll } type;
std::string key;
std::string old_value;
std::string new_value;
std::string source;
};
TestLevelDBObserver() : binding_(this) {}
blink::mojom::StorageAreaObserverAssociatedPtrInfo Bind() {
blink::mojom::StorageAreaObserverAssociatedPtrInfo ptr_info;
binding_.Bind(mojo::MakeRequest(&ptr_info));
return ptr_info;
}
const std::vector<Observation>& observations() { return observations_; }
private:
void KeyAdded(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& value,
const std::string& source) override {
observations_.push_back({Observation::kAdd, Uint8VectorToStdString(key), "",
Uint8VectorToStdString(value), source});
}
void KeyChanged(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& new_value,
const std::vector<uint8_t>& old_value,
const std::string& source) override {
observations_.push_back({Observation::kChange, Uint8VectorToStdString(key),
Uint8VectorToStdString(old_value),
Uint8VectorToStdString(new_value), source});
}
void KeyDeleted(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& old_value,
const std::string& source) override {
observations_.push_back({Observation::kDelete, Uint8VectorToStdString(key),
Uint8VectorToStdString(old_value), "", source});
}
void AllDeleted(const std::string& source) override {
observations_.push_back({Observation::kDeleteAll, "", "", "", source});
}
void ShouldSendOldValueOnMutations(bool value) override {}
std::vector<Observation> observations_;
mojo::AssociatedBinding<blink::mojom::StorageAreaObserver> binding_;
};
} // namespace
class LocalStorageContextMojoTest : public testing::Test {
public:
LocalStorageContextMojoTest()
: db_(&mock_data_),
db_binding_(&db_),
task_runner_(new MockDOMStorageTaskRunner(
base::ThreadTaskRunnerHandle::Get().get())),
mock_special_storage_policy_(new MockSpecialStoragePolicy()) {
EXPECT_TRUE(temp_path_.CreateUniqueTempDir());
dom_storage_context_ =
new DOMStorageContextImpl(base::FilePath(), nullptr, task_runner_);
}
~LocalStorageContextMojoTest() override {
if (dom_storage_context_)
dom_storage_context_->Shutdown();
if (context_)
ShutdownContext();
}
LocalStorageContextMojo* context() {
if (!context_) {
context_ = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), nullptr, task_runner_,
temp_path_.GetPath(), base::FilePath(FILE_PATH_LITERAL("leveldb")),
special_storage_policy());
leveldb::mojom::LevelDBDatabaseAssociatedPtr database_ptr;
leveldb::mojom::LevelDBDatabaseAssociatedRequest request =
MakeRequestAssociatedWithDedicatedPipe(&database_ptr);
context_->SetDatabaseForTesting(std::move(database_ptr));
db_binding_.Bind(std::move(request));
}
return context_;
}
void ShutdownContext() {
context_->ShutdownAndDelete();
context_ = nullptr;
base::RunLoop().RunUntilIdle();
}
DOMStorageNamespace* local_storage_namespace() {
return dom_storage_context_->GetStorageNamespace(kLocalStorageNamespaceId);
}
MockSpecialStoragePolicy* special_storage_policy() {
return mock_special_storage_policy_.get();
}
const std::map<std::vector<uint8_t>, std::vector<uint8_t>>& mock_data() {
return mock_data_;
}
void clear_mock_data() { mock_data_.clear(); }
void set_mock_data(const std::string& key, const std::string& value) {
mock_data_[StdStringToUint8Vector(key)] = StdStringToUint8Vector(value);
}
std::vector<StorageUsageInfo> GetStorageUsageSync() {
base::RunLoop run_loop;
std::vector<StorageUsageInfo> result;
context()->GetStorageUsage(base::BindOnce(&GetStorageUsageCallback,
run_loop.QuitClosure(), &result));
run_loop.Run();
return result;
}
base::Optional<std::vector<uint8_t>> DoTestGet(
const std::vector<uint8_t>& key) {
const url::Origin kOrigin = url::Origin::Create(GURL("http://foobar.com"));
blink::mojom::StorageAreaPtr area;
blink::mojom::StorageAreaPtr dummy_area; // To make sure values are cached.
context()->OpenLocalStorage(kOrigin, MakeRequest(&area));
context()->OpenLocalStorage(kOrigin, MakeRequest(&dummy_area));
std::vector<uint8_t> result;
bool success = test::GetSync(area.get(), key, &result);
return success ? base::Optional<std::vector<uint8_t>>(result)
: base::nullopt;
}
void CloseBinding() { db_binding_.Close(); }
base::FilePath TempPath() { return temp_path_.GetPath(); }
private:
TestBrowserThreadBundle thread_bundle_;
base::ScopedTempDir temp_path_;
std::map<std::vector<uint8_t>, std::vector<uint8_t>> mock_data_;
FakeLevelDBDatabase db_;
mojo::AssociatedBinding<leveldb::mojom::LevelDBDatabase> db_binding_;
scoped_refptr<MockDOMStorageTaskRunner> task_runner_;
scoped_refptr<DOMStorageContextImpl> dom_storage_context_;
LocalStorageContextMojo* context_ = nullptr;
scoped_refptr<MockSpecialStoragePolicy> mock_special_storage_policy_;
DISALLOW_COPY_AND_ASSIGN(LocalStorageContextMojoTest);
};
TEST_F(LocalStorageContextMojoTest, Basic) {
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
base::RunLoop().RunUntilIdle();
// Should have three rows of data, one for the version, one for the actual
// data and one for metadata.
ASSERT_EQ(3u, mock_data().size());
}
TEST_F(LocalStorageContextMojoTest, OriginsAreIndependent) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com:123"));
url::Origin origin2 = url::Origin::Create(GURL("http://foobar.com:1234"));
auto key1 = StdStringToUint8Vector("4key");
auto key2 = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key1, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key2, value, base::nullopt, "source", base::DoNothing());
area.reset();
base::RunLoop().RunUntilIdle();
ASSERT_EQ(5u, mock_data().size());
}
TEST_F(LocalStorageContextMojoTest, WrapperOutlivesMojoConnection) {
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
// Write some data to the DB.
blink::mojom::StorageAreaPtr area;
blink::mojom::StorageAreaPtr dummy_area; // To make sure values are cached.
const url::Origin kOrigin(url::Origin::Create(GURL("http://foobar.com")));
context()->OpenLocalStorage(kOrigin, MakeRequest(&area));
context()->OpenLocalStorage(kOrigin, MakeRequest(&dummy_area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
dummy_area.reset();
base::RunLoop().RunUntilIdle();
// Clear all the data from the backing database.
EXPECT_FALSE(mock_data().empty());
clear_mock_data();
// Data should still be readable, because despite closing the area
// connection above, the actual area instance should have been kept alive.
EXPECT_EQ(value, DoTestGet(key));
// Now purge memory.
context()->PurgeMemory();
// And make sure caches were actually cleared.
EXPECT_EQ(base::nullopt, DoTestGet(key));
}
TEST_F(LocalStorageContextMojoTest, OpeningWrappersPurgesInactiveWrappers) {
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
// Write some data to the DB.
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
base::RunLoop().RunUntilIdle();
// Clear all the data from the backing database.
EXPECT_FALSE(mock_data().empty());
clear_mock_data();
// Now open many new areas (for different origins) to trigger clean up.
for (int i = 1; i <= 100; ++i) {
context()->OpenLocalStorage(url::Origin::Create(GURL(base::StringPrintf(
"http://example.com:%d", i))),
MakeRequest(&area));
area.reset();
}
// And make sure caches were actually cleared.
EXPECT_EQ(base::nullopt, DoTestGet(key));
}
TEST_F(LocalStorageContextMojoTest, ValidVersion) {
set_mock_data("VERSION", "1");
set_mock_data(std::string("_http://foobar.com") + '\x00' + "key", "value");
EXPECT_EQ(StdStringToUint8Vector("value"),
DoTestGet(StdStringToUint8Vector("key")));
}
TEST_F(LocalStorageContextMojoTest, InvalidVersion) {
set_mock_data("VERSION", "foobar");
set_mock_data(std::string("_http://foobar.com") + '\x00' + "key", "value");
EXPECT_EQ(base::nullopt, DoTestGet(StdStringToUint8Vector("key")));
}
TEST_F(LocalStorageContextMojoTest, VersionOnlyWrittenOnCommit) {
EXPECT_EQ(base::nullopt, DoTestGet(StdStringToUint8Vector("key")));
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(mock_data().empty());
}
TEST_F(LocalStorageContextMojoTest, GetStorageUsage_NoData) {
std::vector<StorageUsageInfo> info = GetStorageUsageSync();
EXPECT_EQ(0u, info.size());
}
TEST_F(LocalStorageContextMojoTest, GetStorageUsage_Data) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
auto key1 = StdStringToUint8Vector("key1");
auto key2 = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
base::Time before_write = base::Time::Now();
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key1, value, base::nullopt, "source", base::DoNothing());
area->Put(key2, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key2, value, base::nullopt, "source", base::DoNothing());
area.reset();
// GetStorageUsage only includes committed data, but still returns all origins
// that used localstorage with zero size.
std::vector<StorageUsageInfo> info = GetStorageUsageSync();
ASSERT_EQ(2u, info.size());
if (info[0].origin == origin2)
std::swap(info[0], info[1]);
EXPECT_EQ(origin1, info[0].origin);
EXPECT_EQ(origin2, info[1].origin);
EXPECT_LE(before_write, info[0].last_modified);
EXPECT_LE(before_write, info[1].last_modified);
EXPECT_EQ(0u, info[0].total_size_bytes);
EXPECT_EQ(0u, info[1].total_size_bytes);
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
base::Time after_write = base::Time::Now();
info = GetStorageUsageSync();
ASSERT_EQ(2u, info.size());
if (info[0].origin == origin2)
std::swap(info[0], info[1]);
EXPECT_EQ(origin1, info[0].origin);
EXPECT_EQ(origin2, info[1].origin);
EXPECT_LE(before_write, info[0].last_modified);
EXPECT_LE(before_write, info[1].last_modified);
EXPECT_GE(after_write, info[0].last_modified);
EXPECT_GE(after_write, info[1].last_modified);
EXPECT_GT(info[0].total_size_bytes, info[1].total_size_bytes);
}
TEST_F(LocalStorageContextMojoTest, MetaDataClearedOnDelete) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Delete(key, value, "source", base::DoNothing());
area.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
// Data from origin2 should exist, including meta-data, but nothing should
// exist for origin1.
EXPECT_EQ(3u, mock_data().size());
for (const auto& it : mock_data()) {
if (Uint8VectorToStdString(it.first) == "VERSION")
continue;
EXPECT_EQ(std::string::npos,
Uint8VectorToStdString(it.first).find(origin1.Serialize()));
EXPECT_NE(std::string::npos,
Uint8VectorToStdString(it.first).find(origin2.Serialize()));
}
}
TEST_F(LocalStorageContextMojoTest, MetaDataClearedOnDeleteAll) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->DeleteAll("source", base::DoNothing());
area.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
// Data from origin2 should exist, including meta-data, but nothing should
// exist for origin1.
EXPECT_EQ(3u, mock_data().size());
for (const auto& it : mock_data()) {
if (Uint8VectorToStdString(it.first) == "VERSION")
continue;
EXPECT_EQ(std::string::npos,
Uint8VectorToStdString(it.first).find(origin1.Serialize()));
EXPECT_NE(std::string::npos,
Uint8VectorToStdString(it.first).find(origin2.Serialize()));
}
}
TEST_F(LocalStorageContextMojoTest, MojoConnectionDisconnects) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
{
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
}
EXPECT_EQ(value, DoTestGet(key));
// Close the database connection.
CloseBinding();
base::RunLoop().RunUntilIdle();
// We can't access the data anymore.
EXPECT_EQ(base::nullopt, DoTestGet(key));
// Check that local storage still works without a database.
{
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
}
EXPECT_EQ(value, DoTestGet(key));
}
TEST_F(LocalStorageContextMojoTest, DeleteStorage) {
set_mock_data("VERSION", "1");
set_mock_data(std::string("_http://foobar.com") + '\x00' + "key", "value");
base::RunLoop run_loop;
context()->DeleteStorage(url::Origin::Create(GURL("http://foobar.com")),
run_loop.QuitClosure());
run_loop.Run();
EXPECT_EQ(1u, mock_data().size());
}
TEST_F(LocalStorageContextMojoTest, DeleteStorageWithoutConnection) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
context()->DeleteStorage(origin1, base::DoNothing());
base::RunLoop().RunUntilIdle();
// Data from origin2 should exist, including meta-data, but nothing should
// exist for origin1.
EXPECT_EQ(3u, mock_data().size());
for (const auto& it : mock_data()) {
if (Uint8VectorToStdString(it.first) == "VERSION")
continue;
EXPECT_EQ(std::string::npos,
Uint8VectorToStdString(it.first).find(origin1.Serialize()));
EXPECT_NE(std::string::npos,
Uint8VectorToStdString(it.first).find(origin2.Serialize()));
}
}
TEST_F(LocalStorageContextMojoTest, DeleteStorageNotifiesWrapper) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
TestLevelDBObserver observer;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->AddObserver(observer.Bind());
base::RunLoop().RunUntilIdle();
context()->DeleteStorage(origin1, base::DoNothing());
base::RunLoop().RunUntilIdle();
ASSERT_EQ(1u, observer.observations().size());
EXPECT_EQ(TestLevelDBObserver::Observation::kDeleteAll,
observer.observations()[0].type);
// Data from origin2 should exist, including meta-data, but nothing should
// exist for origin1.
EXPECT_EQ(3u, mock_data().size());
for (const auto& it : mock_data()) {
if (Uint8VectorToStdString(it.first) == "VERSION")
continue;
EXPECT_EQ(std::string::npos,
Uint8VectorToStdString(it.first).find(origin1.Serialize()));
EXPECT_NE(std::string::npos,
Uint8VectorToStdString(it.first).find(origin2.Serialize()));
}
}
TEST_F(LocalStorageContextMojoTest, DeleteStorageWithPendingWrites) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key, value, base::nullopt, "source", base::DoNothing());
area.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
TestLevelDBObserver observer;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->AddObserver(observer.Bind());
area->Put(StdStringToUint8Vector("key2"), value, base::nullopt, "source",
base::DoNothing());
base::RunLoop().RunUntilIdle();
context()->DeleteStorage(origin1, base::DoNothing());
base::RunLoop().RunUntilIdle();
ASSERT_EQ(2u, observer.observations().size());
EXPECT_EQ(TestLevelDBObserver::Observation::kAdd,
observer.observations()[0].type);
EXPECT_EQ(TestLevelDBObserver::Observation::kDeleteAll,
observer.observations()[1].type);
// Data from origin2 should exist, including meta-data, but nothing should
// exist for origin1.
EXPECT_EQ(3u, mock_data().size());
for (const auto& it : mock_data()) {
if (Uint8VectorToStdString(it.first) == "VERSION")
continue;
EXPECT_EQ(std::string::npos,
Uint8VectorToStdString(it.first).find(origin1.Serialize()));
EXPECT_NE(std::string::npos,
Uint8VectorToStdString(it.first).find(origin2.Serialize()));
}
}
TEST_F(LocalStorageContextMojoTest, Migration) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
base::string16 key = base::ASCIIToUTF16("key");
base::string16 value = base::ASCIIToUTF16("value");
base::string16 key2 = base::ASCIIToUTF16("key2");
key2.push_back(0xd83d);
key2.push_back(0xde00);
base::FilePath old_db_path =
TempPath().Append(DOMStorageArea::DatabaseFileNameFromOrigin(origin1));
{
DOMStorageDatabase db(old_db_path);
DOMStorageValuesMap data;
data[key] = base::NullableString16(value, false);
data[key2] = base::NullableString16(value, false);
db.CommitChanges(false, data);
}
EXPECT_TRUE(base::PathExists(old_db_path));
// Opening origin2 and accessing its data should not migrate anything.
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin2, MakeRequest(&area));
blink::mojom::StorageAreaPtr dummy_area; // To make sure values are cached.
context()->OpenLocalStorage(origin2, MakeRequest(&dummy_area));
area->Get(std::vector<uint8_t>(), base::DoNothing());
area.reset();
dummy_area.reset();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(mock_data().empty());
// Opening origin1 and accessing its data should migrate its storage.
context()->OpenLocalStorage(origin1, MakeRequest(&area));
context()->OpenLocalStorage(origin1, MakeRequest(&dummy_area));
area->Get(std::vector<uint8_t>(), base::DoNothing());
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
{
std::vector<uint8_t> result;
bool success = test::GetSync(
area.get(), LocalStorageContextMojo::MigrateString(key), &result);
EXPECT_TRUE(success);
EXPECT_EQ(LocalStorageContextMojo::MigrateString(value), result);
}
{
std::vector<uint8_t> result;
bool success = test::GetSync(
area.get(), LocalStorageContextMojo::MigrateString(key2), &result);
EXPECT_TRUE(success);
EXPECT_EQ(LocalStorageContextMojo::MigrateString(value), result);
}
// Origin1 should no longer exist in old storage.
EXPECT_FALSE(base::PathExists(old_db_path));
}
static std::string EncodeKeyAsUTF16(const std::string& origin,
const base::string16& key) {
std::string result = '_' + origin + '\x00' + '\x00';
std::copy(reinterpret_cast<const char*>(key.data()),
reinterpret_cast<const char*>(key.data()) +
key.size() * sizeof(base::char16),
std::back_inserter(result));
return result;
}
TEST_F(LocalStorageContextMojoTest, FixUp) {
set_mock_data("VERSION", "1");
// Add mock data for the "key" key, with both possible encodings for key.
// We expect the value of the correctly encoded key to take precedence over
// the incorrectly encoded key (and expect the incorrectly encoded key to be
// deleted.
set_mock_data(std::string("_http://foobar.com") + '\x00' + "\x01key",
"value1");
set_mock_data(
EncodeKeyAsUTF16("http://foobar.com", base::ASCIIToUTF16("key")),
"value2");
// Also add mock data for the "foo" key, this time only with the incorrec
// encoding. This should be updated to the correct encoding.
set_mock_data(
EncodeKeyAsUTF16("http://foobar.com", base::ASCIIToUTF16("foo")),
"value3");
blink::mojom::StorageAreaPtr area;
blink::mojom::StorageAreaPtr dummy_area; // To make sure values are cached.
context()->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
context()->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&dummy_area));
{
std::vector<uint8_t> result;
bool success = test::GetSync(
area.get(), leveldb::StdStringToUint8Vector("\x01key"), &result);
EXPECT_TRUE(success);
EXPECT_EQ(leveldb::StdStringToUint8Vector("value1"), result);
}
{
std::vector<uint8_t> result;
bool success = test::GetSync(area.get(),
leveldb::StdStringToUint8Vector("\x01"
"foo"),
&result);
EXPECT_TRUE(success);
EXPECT_EQ(leveldb::StdStringToUint8Vector("value3"), result);
}
// Expect 4 rows in the database: VERSION, meta-data for the origin, and two
// rows of actual data.
EXPECT_EQ(4u, mock_data().size());
EXPECT_EQ(leveldb::StdStringToUint8Vector("value1"),
mock_data().rbegin()->second);
EXPECT_EQ(leveldb::StdStringToUint8Vector("value3"),
std::next(mock_data().rbegin())->second);
}
TEST_F(LocalStorageContextMojoTest, ShutdownClearsData) {
url::Origin origin1 = url::Origin::Create(GURL("http://foobar.com"));
url::Origin origin2 = url::Origin::Create(GURL("http://example.com"));
auto key1 = StdStringToUint8Vector("key1");
auto key2 = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context()->OpenLocalStorage(origin1, MakeRequest(&area));
area->Put(key1, value, base::nullopt, "source", base::DoNothing());
area->Put(key2, value, base::nullopt, "source", base::DoNothing());
area.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&area));
area->Put(key2, value, base::nullopt, "source", base::DoNothing());
area.reset();
// Make sure all data gets committed to the DB.
base::RunLoop().RunUntilIdle();
special_storage_policy()->AddSessionOnly(origin1.GetURL());
ShutdownContext();
// Data from origin2 should exist, including meta-data, but nothing should
// exist for origin1.
EXPECT_EQ(3u, mock_data().size());
for (const auto& it : mock_data()) {
if (Uint8VectorToStdString(it.first) == "VERSION")
continue;
EXPECT_EQ(std::string::npos,
Uint8VectorToStdString(it.first).find(origin1.Serialize()));
EXPECT_NE(std::string::npos,
Uint8VectorToStdString(it.first).find(origin2.Serialize()));
}
}
class LocalStorageContextMojoTestWithService
: public test::MojoTestWithFileService {
public:
LocalStorageContextMojoTestWithService() {}
~LocalStorageContextMojoTestWithService() override {}
protected:
void DoTestPut(LocalStorageContextMojo* context,
const std::vector<uint8_t>& key,
const std::vector<uint8_t>& value) {
blink::mojom::StorageAreaPtr area;
bool success = false;
base::RunLoop run_loop;
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
area->Put(key, value, base::nullopt, "source",
test::MakeSuccessCallback(run_loop.QuitClosure(), &success));
run_loop.Run();
EXPECT_TRUE(success);
area.reset();
RunUntilIdle();
}
bool DoTestGet(LocalStorageContextMojo* context,
const std::vector<uint8_t>& key,
std::vector<uint8_t>* result) {
blink::mojom::StorageAreaPtr area;
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
base::RunLoop run_loop;
std::vector<blink::mojom::KeyValuePtr> data;
bool success = false;
bool done = false;
area->GetAll(
test::GetAllCallback::CreateAndBind(&done, run_loop.QuitClosure()),
test::MakeGetAllCallback(&success, &data));
run_loop.Run();
EXPECT_TRUE(done);
EXPECT_TRUE(success);
for (auto& entry : data) {
if (key == entry->key) {
*result = std::move(entry->value);
return true;
}
}
result->clear();
return false;
}
base::FilePath FirstEntryInDir() {
base::FileEnumerator enumerator(
temp_path(), false /* recursive */,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
return enumerator.Next();
}
private:
// testing::Test:
void TearDown() override {
// Some of these tests close message pipes which serve as master interfaces
// to other associated interfaces; this in turn schedules tasks to invoke
// the associated interfaces' error handlers, and local storage code relies
// on those handlers running in order to avoid memory leaks at shutdown.
RunUntilIdle();
}
DISALLOW_COPY_AND_ASSIGN(LocalStorageContextMojoTestWithService);
};
TEST_F(LocalStorageContextMojoTestWithService, InMemory) {
auto* context = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), connector(), nullptr,
base::FilePath(), base::FilePath(), nullptr);
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
context = nullptr;
RunUntilIdle();
// Should not have created any files.
EXPECT_TRUE(FirstEntryInDir().empty());
// Re-opening should get fresh data.
context = new LocalStorageContextMojo(base::ThreadTaskRunnerHandle::Get(),
connector(), nullptr, base::FilePath(),
base::FilePath(), nullptr);
EXPECT_FALSE(DoTestGet(context, key, &result));
context->ShutdownAndDelete();
}
TEST_F(LocalStorageContextMojoTestWithService, InMemoryInvalidPath) {
auto* context = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), connector(), nullptr,
base::FilePath(), base::FilePath(FILE_PATH_LITERAL("../../")), nullptr);
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
blink::mojom::StorageAreaPtr area;
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
context = nullptr;
RunUntilIdle();
// Should not have created any files.
EXPECT_TRUE(FirstEntryInDir().empty());
}
TEST_F(LocalStorageContextMojoTestWithService, OnDisk) {
base::FilePath test_path(FILE_PATH_LITERAL("test_path"));
auto* context = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), connector(), nullptr,
base::FilePath(), test_path, nullptr);
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
context = nullptr;
RunUntilIdle();
// Should have created files.
EXPECT_EQ(test_path, FirstEntryInDir().BaseName());
// Should be able to re-open.
context = new LocalStorageContextMojo(base::ThreadTaskRunnerHandle::Get(),
connector(), nullptr, base::FilePath(),
test_path, nullptr);
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
}
TEST_F(LocalStorageContextMojoTestWithService, InvalidVersionOnDisk) {
base::FilePath test_path(FILE_PATH_LITERAL("test_path"));
// Create context and add some data to it.
auto* context = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), connector(), nullptr,
base::FilePath(), test_path, nullptr);
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
context = nullptr;
RunUntilIdle();
{
// Mess up version number in database.
leveldb_env::ChromiumEnv env;
std::unique_ptr<leveldb::DB> db;
leveldb_env::Options options;
options.env = &env;
base::FilePath db_path =
temp_path().Append(test_path).Append(FILE_PATH_LITERAL("leveldb"));
ASSERT_TRUE(leveldb_env::OpenDB(options, db_path.AsUTF8Unsafe(), &db).ok());
ASSERT_TRUE(db->Put(leveldb::WriteOptions(), "VERSION", "argh").ok());
}
// Make sure data is gone.
context = new LocalStorageContextMojo(base::ThreadTaskRunnerHandle::Get(),
connector(), nullptr, base::FilePath(),
test_path, nullptr);
EXPECT_FALSE(DoTestGet(context, key, &result));
// Write data again.
DoTestPut(context, key, value);
context->ShutdownAndDelete();
context = nullptr;
RunUntilIdle();
// Data should have been preserved now.
context = new LocalStorageContextMojo(base::ThreadTaskRunnerHandle::Get(),
connector(), nullptr, base::FilePath(),
test_path, nullptr);
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
}
TEST_F(LocalStorageContextMojoTestWithService, CorruptionOnDisk) {
base::FilePath test_path(FILE_PATH_LITERAL("test_path"));
// Create context and add some data to it.
auto* context = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), connector(), nullptr,
base::FilePath(), test_path, nullptr);
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
context = nullptr;
RunUntilIdle();
// Delete manifest files to mess up opening DB.
base::FilePath db_path =
temp_path().Append(test_path).Append(FILE_PATH_LITERAL("leveldb"));
base::FileEnumerator file_enum(db_path, true, base::FileEnumerator::FILES,
FILE_PATH_LITERAL("MANIFEST*"));
for (base::FilePath name = file_enum.Next(); !name.empty();
name = file_enum.Next()) {
base::DeleteFile(name, false);
}
// Make sure data is gone.
context = new LocalStorageContextMojo(base::ThreadTaskRunnerHandle::Get(),
connector(), nullptr, base::FilePath(),
test_path, nullptr);
EXPECT_FALSE(DoTestGet(context, key, &result));
// Write data again.
DoTestPut(context, key, value);
context->ShutdownAndDelete();
context = nullptr;
RunUntilIdle();
// Data should have been preserved now.
context = new LocalStorageContextMojo(base::ThreadTaskRunnerHandle::Get(),
connector(), nullptr, base::FilePath(),
test_path, nullptr);
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
}
TEST_F(LocalStorageContextMojoTestWithService, RecreateOnCommitFailure) {
FakeLevelDBService mock_leveldb_service;
file_service()->GetBinderRegistryForTesting()->AddInterface(
leveldb::mojom::LevelDBService::Name_,
base::BindRepeating(&test::FakeLevelDBService::Bind,
base::Unretained(&mock_leveldb_service)));
std::map<std::vector<uint8_t>, std::vector<uint8_t>> test_data;
base::FilePath test_path(FILE_PATH_LITERAL("test_path"));
auto* context = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), connector(), nullptr,
base::FilePath(), test_path, nullptr);
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
// Open three connections to the database. Two to the same origin, and a third
// to a different origin.
blink::mojom::StorageAreaPtr area1;
blink::mojom::StorageAreaPtr area2;
blink::mojom::StorageAreaPtr area3;
{
base::RunLoop loop;
mock_leveldb_service.SetOnOpenCallback(loop.QuitClosure());
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area1));
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area2));
context->OpenLocalStorage(url::Origin::Create(GURL("http://example.com")),
MakeRequest(&area3));
loop.Run();
}
// Add observers to the first two connections.
TestLevelDBObserver observer1;
area1->AddObserver(observer1.Bind());
TestLevelDBObserver observer2;
area2->AddObserver(observer2.Bind());
// Verify one attempt was made to open the database, and connect that request
// with a database implementation that always fails on write.
ASSERT_EQ(1u, mock_leveldb_service.open_requests().size());
auto& open_request = mock_leveldb_service.open_requests()[0];
auto mock_db = mojo::MakeStrongAssociatedBinding(
std::make_unique<FakeLevelDBDatabaseErrorOnWrite>(&test_data),
std::move(open_request.request));
std::move(open_request.callback).Run(leveldb::mojom::DatabaseError::OK);
mock_leveldb_service.open_requests().clear();
// Setup a RunLoop so we can wait until LocalStorageContextMojo tries to
// reconnect to the database, which should happen after several commit
// errors.
base::RunLoop reopen_loop;
mock_leveldb_service.SetOnOpenCallback(reopen_loop.QuitClosure());
// Start a put operation on the third connection before starting to commit
// a lot of data on the first origin. This put operation should result in a
// pending commit that will get cancelled when the database connection is
// closed.
area3->Put(key, value, base::nullopt, "source",
base::BindOnce([](bool success) { EXPECT_TRUE(success); }));
// Repeatedly write data to the database, to trigger enough commit errors.
size_t values_written = 0;
while (!area1.encountered_error()) {
base::RunLoop put_loop;
// Every write needs to be different to make sure there actually is a
// change to commit.
value[0]++;
area1.set_connection_error_handler(put_loop.QuitClosure());
area1->Put(key, value, base::nullopt, "source",
base::BindLambdaForTesting([&](bool success) {
EXPECT_TRUE(success);
}));
put_loop.RunUntilIdle();
values_written++;
// And we need to flush after every change. Otherwise changes get batched up
// and only one commit is done some time later.
context->FlushOriginForTesting(
url::Origin::Create(GURL("http://foobar.com")));
}
// Make sure all messages to the DB have been processed (Flush above merely
// schedules a commit, but there is no guarantee about those having been
// processed yet).
mock_leveldb_service.FlushBindingsForTesting();
if (mock_db)
mock_db->FlushForTesting();
// At this point enough commit failures should have happened to cause the
// connection to the database to have been severed.
EXPECT_FALSE(mock_db);
// The connection to the second area should have closed as well.
EXPECT_TRUE(area2.encountered_error());
// And the old database should have been destroyed.
EXPECT_EQ(1u, mock_leveldb_service.destroy_requests().size());
// Reconnect area1 to the database, and try to read a value.
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area1));
base::RunLoop delete_loop;
bool success = true;
TestLevelDBObserver observer3;
area1->AddObserver(observer3.Bind());
area1->Delete(key, base::nullopt, "source",
base::BindLambdaForTesting([&](bool success_in) {
success = success_in;
delete_loop.Quit();
}));
// Wait for LocalStorageContextMojo to try to reconnect to the database, and
// connect that new request to a properly functioning database.
reopen_loop.Run();
ASSERT_EQ(1u, mock_leveldb_service.open_requests().size());
auto& reopen_request = mock_leveldb_service.open_requests()[0];
mock_db = mojo::MakeStrongAssociatedBinding(
std::make_unique<FakeLevelDBDatabase>(&test_data),
std::move(reopen_request.request));
std::move(reopen_request.callback).Run(leveldb::mojom::DatabaseError::OK);
mock_leveldb_service.open_requests().clear();
// And deleting the value from the new area should have failed (as the
// database is empty).
delete_loop.Run();
EXPECT_EQ(0u, observer3.observations().size());
area1 = nullptr;
{
// Committing data should now work.
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
EXPECT_FALSE(test_data.empty());
}
// Observers should have seen one Add event and a number of Change events for
// all commits until the connection was closed.
ASSERT_EQ(values_written, observer2.observations().size());
for (size_t i = 0; i < values_written; ++i) {
EXPECT_EQ(i ? TestLevelDBObserver::Observation::kChange
: TestLevelDBObserver::Observation::kAdd,
observer2.observations()[i].type);
EXPECT_EQ(Uint8VectorToStdString(key), observer2.observations()[i].key);
}
}
TEST_F(LocalStorageContextMojoTestWithService,
DontRecreateOnRepeatedCommitFailure) {
FakeLevelDBService mock_leveldb_service;
file_service()->GetBinderRegistryForTesting()->AddInterface(
leveldb::mojom::LevelDBService::Name_,
base::BindRepeating(&test::FakeLevelDBService::Bind,
base::Unretained(&mock_leveldb_service)));
std::map<std::vector<uint8_t>, std::vector<uint8_t>> test_data;
base::FilePath test_path(FILE_PATH_LITERAL("test_path"));
auto* context = new LocalStorageContextMojo(
base::ThreadTaskRunnerHandle::Get(), connector(), nullptr,
base::FilePath(), test_path, nullptr);
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
// Open a connection to the database.
blink::mojom::StorageAreaPtr area;
{
base::RunLoop loop;
mock_leveldb_service.SetOnOpenCallback(loop.QuitClosure());
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
loop.Run();
}
// Verify one attempt was made to open the database, and connect that request
// with a database implementation that always fails on write.
ASSERT_EQ(1u, mock_leveldb_service.open_requests().size());
auto& open_request = mock_leveldb_service.open_requests()[0];
auto mock_db = mojo::MakeStrongAssociatedBinding(
std::make_unique<FakeLevelDBDatabaseErrorOnWrite>(&test_data),
std::move(open_request.request));
std::move(open_request.callback).Run(leveldb::mojom::DatabaseError::OK);
mock_leveldb_service.open_requests().clear();
// Setup a RunLoop so we can wait until LocalStorageContextMojo tries to
// reconnect to the database, which should happen after several commit
// errors.
base::RunLoop reopen_loop;
mock_leveldb_service.SetOnOpenCallback(reopen_loop.QuitClosure());
// Repeatedly write data to the database, to trigger enough commit errors.
base::Optional<std::vector<uint8_t>> old_value;
while (!area.encountered_error()) {
base::RunLoop put_loop;
// Every write needs to be different to make sure there actually is a
// change to commit.
value[0]++;
area.set_connection_error_handler(put_loop.QuitClosure());
area->Put(key, value, old_value, "source",
base::BindLambdaForTesting([&](bool success) {
EXPECT_TRUE(success);
put_loop.Quit();
}));
old_value = std::vector<uint8_t>(value);
put_loop.RunUntilIdle();
// And we need to flush after every change. Otherwise changes get batched up
// and only one commit is done some time later.
context->FlushOriginForTesting(
url::Origin::Create(GURL("http://foobar.com")));
}
// Make sure all messages to the DB have been processed (Flush above merely
// schedules a commit, but there is no guarantee about those having been
// processed yet).
if (mock_db)
mock_db->FlushForTesting();
// At this point enough commit failures should have happened to cause the
// connection to the database to have been severed.
EXPECT_FALSE(mock_db);
// Wait for LocalStorageContextMojo to try to reconnect to the database, and
// connect that new request with a database implementation that always fails
// on write.
reopen_loop.Run();
ASSERT_EQ(1u, mock_leveldb_service.open_requests().size());
auto& reopen_request = mock_leveldb_service.open_requests()[0];
mock_db = mojo::MakeStrongAssociatedBinding(
std::make_unique<FakeLevelDBDatabaseErrorOnWrite>(&test_data),
std::move(reopen_request.request));
std::move(reopen_request.callback).Run(leveldb::mojom::DatabaseError::OK);
mock_leveldb_service.open_requests().clear();
// The old database should also have been destroyed.
EXPECT_EQ(1u, mock_leveldb_service.destroy_requests().size());
// Reconnect a area to the database, and repeatedly write data to it again.
// This time all should just keep getting written, and commit errors are
// getting ignored.
context->OpenLocalStorage(url::Origin::Create(GURL("http://foobar.com")),
MakeRequest(&area));
old_value = base::nullopt;
for (int i = 0; i < 64; ++i) {
base::RunLoop put_loop;
// Every write needs to be different to make sure there actually is a
// change to commit.
value[0]++;
area.set_connection_error_handler(put_loop.QuitClosure());
area->Put(key, value, old_value, "source",
base::BindLambdaForTesting([&](bool success) {
EXPECT_TRUE(success);
put_loop.Quit();
}));
put_loop.RunUntilIdle();
old_value = value;
// And we need to flush after every change. Otherwise changes get batched up
// and only one commit is done some time later.
context->FlushOriginForTesting(
url::Origin::Create(GURL("http://foobar.com")));
}
// Make sure all messages to the DB have been processed (Flush above merely
// schedules a commit, but there is no guarantee about those having been
// processed yet).
if (mock_db)
mock_db->FlushForTesting();
EXPECT_TRUE(mock_db);
EXPECT_FALSE(area.encountered_error());
}
} // namespace content