blob: c481f52df82c23307c95432d5941f80e971d6274 [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/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 "build/build_config.h"
#include "components/filesystem/public/interfaces/file_system.mojom.h"
#include "components/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_namespace.h"
#include "content/browser/dom_storage/dom_storage_task_runner.h"
#include "content/common/dom_storage/dom_storage_types.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/local_storage_usage_info.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_utils.h"
#include "content/test/mock_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/file_service.h"
#include "services/file/public/interfaces/constants.mojom.h"
#include "services/file/user_id_map.h"
#include "services/service_manager/public/cpp/service_context.h"
#include "services/service_manager/public/cpp/service_test.h"
#include "services/service_manager/public/interfaces/service_factory.mojom.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 {
void NoOpSuccess(bool success) {}
void GetStorageUsageCallback(const base::Closure& callback,
std::vector<LocalStorageUsageInfo>* out_result,
std::vector<LocalStorageUsageInfo> result) {
*out_result = std::move(result);
callback.Run();
}
void GetCallback(const base::Closure& callback,
bool* success_out,
std::vector<uint8_t>* value_out,
bool success,
const std::vector<uint8_t>& value) {
*success_out = success;
*value_out = value;
callback.Run();
}
void NoOpGet(bool success, const std::vector<uint8_t>& value) {}
class TestLevelDBObserver : public mojom::LevelDBObserver {
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) {}
mojom::LevelDBObserverAssociatedPtrInfo Bind() {
mojom::LevelDBObserverAssociatedPtrInfo 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});
}
std::vector<Observation> observations_;
mojo::AssociatedBinding<mojom::LevelDBObserver> binding_;
};
class GetAllCallback : public mojom::LevelDBWrapperGetAllCallback {
public:
static mojom::LevelDBWrapperGetAllCallbackAssociatedPtrInfo CreateAndBind() {
mojom::LevelDBWrapperGetAllCallbackAssociatedPtrInfo ptr_info;
auto request = mojo::MakeRequest(&ptr_info);
mojo::MakeStrongAssociatedBinding(base::WrapUnique(new GetAllCallback),
std::move(request));
return ptr_info;
}
private:
GetAllCallback() {}
void Complete(bool success) override {}
};
} // 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(
temp_path_.GetPath(), 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 =
MakeIsolatedRequest(&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();
}
void FlushAndPurgeDOMStorageMemory() {
dom_storage_context_->Flush();
base::RunLoop().RunUntilIdle();
dom_storage_context_->PurgeMemory(DOMStorageContextImpl::PURGE_AGGRESSIVE);
}
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<LocalStorageUsageInfo> GetStorageUsageSync() {
base::RunLoop run_loop;
std::vector<LocalStorageUsageInfo> result;
context()->GetStorageUsage(base::BindOnce(&GetStorageUsageCallback,
run_loop.QuitClosure(), &result));
run_loop.Run();
return result;
}
private:
TestBrowserThreadBundle thread_bundle_;
base::ScopedTempDir temp_path_;
std::map<std::vector<uint8_t>, std::vector<uint8_t>> mock_data_;
MockLevelDBDatabase 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");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.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(GURL("http://foobar.com:123"));
url::Origin origin2(GURL("http://foobar.com:1234"));
auto key1 = StdStringToUint8Vector("4key");
auto key2 = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key1, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key2, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.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.
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.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 wrapper
// connection above, the actual wrapper instance should have been kept alive.
{
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
wrapper->Get(key, base::BindOnce(&GetCallback, run_loop.QuitClosure(),
&success, &result));
run_loop.Run();
EXPECT_TRUE(success);
EXPECT_EQ(value, result);
wrapper.reset();
}
// Now purge memory.
context()->PurgeMemory();
// And make sure caches were actually cleared.
{
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
wrapper->Get(key, base::BindOnce(&GetCallback, run_loop.QuitClosure(),
&success, &result));
run_loop.Run();
EXPECT_FALSE(success);
wrapper.reset();
}
}
TEST_F(LocalStorageContextMojoTest, OpeningWrappersPurgesInactiveWrappers) {
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
// Write some data to the DB.
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
base::RunLoop().RunUntilIdle();
// Clear all the data from the backing database.
EXPECT_FALSE(mock_data().empty());
clear_mock_data();
// Now open many new wrappers (for different origins) to trigger clean up.
for (int i = 1; i <= 100; ++i) {
context()->OpenLocalStorage(
url::Origin(GURL(base::StringPrintf("http://example.com:%d", i))),
MakeRequest(&wrapper));
wrapper.reset();
}
// And make sure caches were actually cleared.
base::RunLoop run_loop;
bool success = true;
std::vector<uint8_t> result;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
wrapper->Get(key, base::BindOnce(&GetCallback, run_loop.QuitClosure(),
&success, &result));
run_loop.Run();
EXPECT_FALSE(success);
wrapper.reset();
}
TEST_F(LocalStorageContextMojoTest, ValidVersion) {
set_mock_data("VERSION", "1");
set_mock_data(std::string("_http://foobar.com") + '\x00' + "key", "value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
wrapper->Get(
StdStringToUint8Vector("key"),
base::BindOnce(&GetCallback, run_loop.QuitClosure(), &success, &result));
run_loop.Run();
EXPECT_TRUE(success);
EXPECT_EQ(StdStringToUint8Vector("value"), result);
}
TEST_F(LocalStorageContextMojoTest, InvalidVersion) {
set_mock_data("VERSION", "foobar");
set_mock_data(std::string("_http://foobar.com") + '\x00' + "key", "value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
wrapper->Get(
StdStringToUint8Vector("key"),
base::BindOnce(&GetCallback, run_loop.QuitClosure(), &success, &result));
run_loop.Run();
EXPECT_FALSE(success);
}
TEST_F(LocalStorageContextMojoTest, VersionOnlyWrittenOnCommit) {
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
wrapper->Get(
StdStringToUint8Vector("key"),
base::BindOnce(&GetCallback, run_loop.QuitClosure(), &success, &result));
run_loop.Run();
EXPECT_FALSE(success);
wrapper.reset();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(mock_data().empty());
}
TEST_F(LocalStorageContextMojoTest, GetStorageUsage_NoData) {
std::vector<LocalStorageUsageInfo> info = GetStorageUsageSync();
EXPECT_EQ(0u, info.size());
}
TEST_F(LocalStorageContextMojoTest, GetStorageUsage_Data) {
url::Origin origin1(GURL("http://foobar.com"));
url::Origin origin2(GURL("http://example.com"));
auto key1 = StdStringToUint8Vector("key1");
auto key2 = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
base::Time before_write = base::Time::Now();
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key1, value, "source", base::BindOnce(&NoOpSuccess));
wrapper->Put(key2, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key2, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
// GetStorageUsage only includes committed data, but still returns all origins
// that used localstorage with zero size.
std::vector<LocalStorageUsageInfo> info = GetStorageUsageSync();
ASSERT_EQ(2u, info.size());
if (url::Origin(info[0].origin) == origin2)
std::swap(info[0], info[1]);
EXPECT_EQ(origin1, url::Origin(info[0].origin));
EXPECT_EQ(origin2, url::Origin(info[1].origin));
EXPECT_LE(before_write, info[0].last_modified);
EXPECT_LE(before_write, info[1].last_modified);
EXPECT_EQ(0u, info[0].data_size);
EXPECT_EQ(0u, info[1].data_size);
// 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 (url::Origin(info[0].origin) == origin2)
std::swap(info[0], info[1]);
EXPECT_EQ(origin1, url::Origin(info[0].origin));
EXPECT_EQ(origin2, url::Origin(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].data_size, info[1].data_size);
}
TEST_F(LocalStorageContextMojoTest, MetaDataClearedOnDelete) {
url::Origin origin1(GURL("http://foobar.com"));
url::Origin origin2(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Delete(key, "source", base::BindOnce(&NoOpSuccess));
wrapper.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(GURL("http://foobar.com"));
url::Origin origin2(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->DeleteAll("source", base::BindOnce(&NoOpSuccess));
wrapper.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, DeleteStorage) {
set_mock_data("VERSION", "1");
set_mock_data(std::string("_http://foobar.com") + '\x00' + "key", "value");
context()->DeleteStorage(url::Origin(GURL("http://foobar.com")));
base::RunLoop().RunUntilIdle();
EXPECT_EQ(1u, mock_data().size());
}
TEST_F(LocalStorageContextMojoTest, DeleteStorageWithoutConnection) {
url::Origin origin1(GURL("http://foobar.com"));
url::Origin origin2(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
context()->DeleteStorage(origin1);
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(GURL("http://foobar.com"));
url::Origin origin2(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
TestLevelDBObserver observer;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->AddObserver(observer.Bind());
base::RunLoop().RunUntilIdle();
context()->DeleteStorage(origin1);
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(GURL("http://foobar.com"));
url::Origin origin2(GURL("http://example.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
TestLevelDBObserver observer;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->AddObserver(observer.Bind());
wrapper->Put(StdStringToUint8Vector("key2"), value, "source",
base::BindOnce(&NoOpSuccess));
base::RunLoop().RunUntilIdle();
context()->DeleteStorage(origin1);
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, DeleteStorageForPhysicalOrigin) {
url::Origin origin1a(GURL("http://foobar.com"));
url::Origin origin1b(GURL("http-so://suborigin.foobar.com"));
url::Origin origin2(GURL("https://foobar.com"));
auto key = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1a, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin1b, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
// Make sure all data gets committed to disk.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
context()->DeleteStorageForPhysicalOrigin(origin1b);
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(origin1a.Serialize()));
EXPECT_EQ(std::string::npos,
Uint8VectorToStdString(it.first).find(origin1b.Serialize()));
EXPECT_NE(std::string::npos,
Uint8VectorToStdString(it.first).find(origin2.Serialize()));
}
}
TEST_F(LocalStorageContextMojoTest, Migration) {
url::Origin origin1(GURL("http://foobar.com"));
url::Origin origin2(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);
DOMStorageNamespace* local = local_storage_namespace();
DOMStorageArea* area = local->OpenStorageArea(origin1.GetURL());
base::NullableString16 dummy;
area->SetItem(key, value, dummy, &dummy);
area->SetItem(key2, value, dummy, &dummy);
local->CloseStorageArea(area);
FlushAndPurgeDOMStorageMemory();
// Opening origin2 and accessing its data should not migrate anything.
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Get(std::vector<uint8_t>(), base::BindOnce(&NoOpGet));
wrapper.reset();
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(mock_data().empty());
// Opening origin1 and accessing its data should migrate its storage.
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Get(std::vector<uint8_t>(), base::BindOnce(&NoOpGet));
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(mock_data().empty());
{
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
wrapper->Get(LocalStorageContextMojo::MigrateString(key),
base::BindOnce(&GetCallback, run_loop.QuitClosure(), &success,
&result));
run_loop.Run();
EXPECT_TRUE(success);
EXPECT_EQ(LocalStorageContextMojo::MigrateString(value), result);
}
{
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
wrapper->Get(LocalStorageContextMojo::MigrateString(key2),
base::BindOnce(&GetCallback, run_loop.QuitClosure(), &success,
&result));
run_loop.Run();
EXPECT_TRUE(success);
EXPECT_EQ(LocalStorageContextMojo::MigrateString(value), result);
}
// Origin1 should no longer exist in old storage.
area = local->OpenStorageArea(origin1.GetURL());
ASSERT_EQ(0u, area->Length());
local->CloseStorageArea(area);
}
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");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
{
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
wrapper->Get(leveldb::StdStringToUint8Vector("\x01key"),
base::BindOnce(&GetCallback, run_loop.QuitClosure(), &success,
&result));
run_loop.Run();
EXPECT_TRUE(success);
EXPECT_EQ(leveldb::StdStringToUint8Vector("value1"), result);
}
{
base::RunLoop run_loop;
bool success = false;
std::vector<uint8_t> result;
wrapper->Get(leveldb::StdStringToUint8Vector("\x01"
"foo"),
base::BindOnce(&GetCallback, run_loop.QuitClosure(), &success,
&result));
run_loop.Run();
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(GURL("http://foobar.com"));
url::Origin origin2(GURL("http://example.com"));
auto key1 = StdStringToUint8Vector("key1");
auto key2 = StdStringToUint8Vector("key");
auto value = StdStringToUint8Vector("value");
mojom::LevelDBWrapperPtr wrapper;
context()->OpenLocalStorage(origin1, MakeRequest(&wrapper));
wrapper->Put(key1, value, "source", base::BindOnce(&NoOpSuccess));
wrapper->Put(key2, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
context()->OpenLocalStorage(origin2, MakeRequest(&wrapper));
wrapper->Put(key2, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.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()));
}
}
namespace {
class ServiceTestClient : public service_manager::test::ServiceTestClient,
public service_manager::mojom::ServiceFactory {
public:
explicit ServiceTestClient(service_manager::test::ServiceTest* test)
: service_manager::test::ServiceTestClient(test) {
registry_.AddInterface<service_manager::mojom::ServiceFactory>(base::Bind(
&ServiceTestClient::BindServiceFactoryRequest, base::Unretained(this)));
}
~ServiceTestClient() override {}
protected:
void OnBindInterface(const service_manager::BindSourceInfo& source_info,
const std::string& interface_name,
mojo::ScopedMessagePipeHandle interface_pipe) override {
registry_.BindInterface(interface_name, std::move(interface_pipe));
}
void CreateService(service_manager::mojom::ServiceRequest request,
const std::string& name) override {
if (name == file::mojom::kServiceName) {
file_service_context_.reset(new service_manager::ServiceContext(
file::CreateFileService(), std::move(request)));
}
}
void BindServiceFactoryRequest(
service_manager::mojom::ServiceFactoryRequest request) {
service_factory_bindings_.AddBinding(this, std::move(request));
}
private:
service_manager::BinderRegistry registry_;
mojo::BindingSet<service_manager::mojom::ServiceFactory>
service_factory_bindings_;
std::unique_ptr<service_manager::ServiceContext> file_service_context_;
};
} // namespace
class LocalStorageContextMojoTestWithService
: public service_manager::test::ServiceTest {
public:
LocalStorageContextMojoTestWithService()
: ServiceTest("content_unittests", false) {}
~LocalStorageContextMojoTestWithService() override {}
protected:
void SetUp() override {
ServiceTest::SetUp();
ASSERT_TRUE(temp_path_.CreateUniqueTempDir());
file::AssociateServiceUserIdWithUserDir(test_userid(),
temp_path_.GetPath());
}
void TearDown() override {
service_manager::ServiceContext::ClearGlobalBindersForTesting(
file::mojom::kServiceName);
ServiceTest::TearDown();
}
std::unique_ptr<service_manager::Service> CreateService() override {
return base::MakeUnique<ServiceTestClient>(this);
}
const base::FilePath& temp_path() { return temp_path_.GetPath(); }
base::FilePath FirstEntryInDir() {
base::FileEnumerator enumerator(
temp_path(), false /* recursive */,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
return enumerator.Next();
}
void DoTestPut(LocalStorageContextMojo* context,
const std::vector<uint8_t>& key,
const std::vector<uint8_t>& value) {
mojom::LevelDBWrapperPtr wrapper;
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
wrapper->Put(key, value, "source", base::BindOnce(&NoOpSuccess));
wrapper.reset();
base::RunLoop().RunUntilIdle();
}
bool DoTestGet(LocalStorageContextMojo* context,
const std::vector<uint8_t>& key,
std::vector<uint8_t>* result) {
mojom::LevelDBWrapperPtr wrapper;
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
base::RunLoop run_loop;
bool success = false;
wrapper->Get(key, base::BindOnce(&GetCallback, run_loop.QuitClosure(),
&success, result));
run_loop.Run();
return success;
}
private:
base::ScopedTempDir temp_path_;
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");
mojom::LevelDBWrapperPtr wrapper;
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
context = nullptr;
base::RunLoop().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");
mojom::LevelDBWrapperPtr wrapper;
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
DoTestPut(context, key, value);
std::vector<uint8_t> result;
EXPECT_TRUE(DoTestGet(context, key, &result));
EXPECT_EQ(value, result);
context->ShutdownAndDelete();
context = nullptr;
base::RunLoop().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;
base::RunLoop().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();
}
// Flaky on Android. https://crbug.com/756550
#if defined(OS_ANDROID)
#define MAYBE_InvalidVersionOnDisk DISABLED_InvalidVersionOnDisk
#else
#define MAYBE_InvalidVersionOnDisk InvalidVersionOnDisk
#endif
TEST_F(LocalStorageContextMojoTestWithService, MAYBE_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;
base::RunLoop().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;
base::RunLoop().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;
base::RunLoop().RunUntilIdle();
// Also flush Task Scheduler tasks to make sure the leveldb is fully closed.
content::RunAllTasksUntilIdle();
// 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;
base::RunLoop().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();
}
namespace {
class MockLevelDBService : public leveldb::mojom::LevelDBService {
public:
void Open(filesystem::mojom::DirectoryPtr,
const std::string& dbname,
const base::Optional<base::trace_event::MemoryAllocatorDumpGuid>&
memory_dump_id,
leveldb::mojom::LevelDBDatabaseAssociatedRequest request,
OpenCallback callback) override {
open_requests_.push_back(
{false, dbname, std::move(request), std::move(callback)});
if (on_open_callback_)
on_open_callback_.Run();
}
void OpenWithOptions(
const leveldb_env::Options& options,
filesystem::mojom::DirectoryPtr,
const std::string& dbname,
const base::Optional<base::trace_event::MemoryAllocatorDumpGuid>&
memory_dump_id,
leveldb::mojom::LevelDBDatabaseAssociatedRequest request,
OpenCallback callback) override {
open_requests_.push_back(
{false, dbname, std::move(request), std::move(callback)});
if (on_open_callback_)
on_open_callback_.Run();
}
void OpenInMemory(
const base::Optional<base::trace_event::MemoryAllocatorDumpGuid>&
memory_dump_id,
leveldb::mojom::LevelDBDatabaseAssociatedRequest request,
OpenCallback callback) override {
open_requests_.push_back(
{true, "", std::move(request), std::move(callback)});
if (on_open_callback_)
on_open_callback_.Run();
}
void Destroy(filesystem::mojom::DirectoryPtr,
const std::string& dbname,
DestroyCallback callback) override {
destroy_requests_.push_back({dbname});
std::move(callback).Run(leveldb::mojom::DatabaseError::OK);
}
struct OpenRequest {
bool in_memory;
std::string dbname;
leveldb::mojom::LevelDBDatabaseAssociatedRequest request;
OpenCallback callback;
};
std::vector<OpenRequest> open_requests_;
base::Closure on_open_callback_;
struct DestroyRequest {
std::string dbname;
};
std::vector<DestroyRequest> destroy_requests_;
void Bind(const std::string& interface_name,
mojo::ScopedMessagePipeHandle interface_pipe,
const service_manager::BindSourceInfo& source_info) {
bindings_.AddBinding(
this, leveldb::mojom::LevelDBServiceRequest(std::move(interface_pipe)));
}
private:
mojo::BindingSet<leveldb::mojom::LevelDBService> bindings_;
};
class MockLevelDBDatabaseErrorOnWrite : public MockLevelDBDatabase {
public:
explicit MockLevelDBDatabaseErrorOnWrite(
std::map<std::vector<uint8_t>, std::vector<uint8_t>>* mock_data)
: MockLevelDBDatabase(mock_data) {}
void Write(std::vector<leveldb::mojom::BatchedOperationPtr> operations,
WriteCallback callback) override {
std::move(callback).Run(leveldb::mojom::DatabaseError::IO_ERROR);
}
};
} // namespace
TEST_F(LocalStorageContextMojoTestWithService, RecreateOnCommitFailure) {
MockLevelDBService mock_leveldb_service;
service_manager::ServiceContext::SetGlobalBinderForTesting(
file::mojom::kServiceName, leveldb::mojom::LevelDBService::Name_,
base::Bind(&MockLevelDBService::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.
mojom::LevelDBWrapperPtr wrapper1;
mojom::LevelDBWrapperPtr wrapper2;
mojom::LevelDBWrapperPtr wrapper3;
{
base::RunLoop loop;
mock_leveldb_service.on_open_callback_ = loop.QuitClosure();
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper1));
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper2));
context->OpenLocalStorage(url::Origin(GURL("http://example.com")),
MakeRequest(&wrapper3));
loop.Run();
}
// Add observers to the first two connections.
TestLevelDBObserver observer1;
wrapper1->AddObserver(observer1.Bind());
TestLevelDBObserver observer2;
wrapper2->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(
base::MakeUnique<MockLevelDBDatabaseErrorOnWrite>(&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.on_open_callback_ = 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.
wrapper3->Put(key, value, "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 (!wrapper1.encountered_error()) {
base::RunLoop put_loop;
// Every write needs to be different to make sure there actually is a
// change to commit.
value[0]++;
wrapper1.set_connection_error_handler(put_loop.QuitClosure());
wrapper1->Put(key, value, "source",
base::BindOnce(
[](base::Closure quit_closure, bool success) {
EXPECT_TRUE(success);
quit_closure.Run();
},
put_loop.QuitClosure()));
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(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);
// The connection to the second wrapper should have closed as well.
EXPECT_TRUE(wrapper2.encountered_error());
// And the old database should have been destroyed.
EXPECT_EQ(1u, mock_leveldb_service.destroy_requests_.size());
// Reconnect wrapper1 to the database, and try to read a value.
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper1));
base::RunLoop get_loop;
std::vector<uint8_t> result;
bool success = true;
wrapper1->Get(key, base::BindOnce(&GetCallback, get_loop.QuitClosure(),
&success, &result));
// 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(
base::MakeUnique<MockLevelDBDatabase>(&test_data),
std::move(reopen_request.request));
std::move(reopen_request.callback).Run(leveldb::mojom::DatabaseError::OK);
mock_leveldb_service.open_requests_.clear();
// And reading the value from the new wrapper should have failed (as the
// database is empty).
get_loop.Run();
EXPECT_FALSE(success);
wrapper1 = 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) {
MockLevelDBService mock_leveldb_service;
service_manager::ServiceContext::SetGlobalBinderForTesting(
file::mojom::kServiceName, leveldb::mojom::LevelDBService::Name_,
base::Bind(&MockLevelDBService::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.
mojom::LevelDBWrapperPtr wrapper;
{
base::RunLoop loop;
mock_leveldb_service.on_open_callback_ = loop.QuitClosure();
context->OpenLocalStorage(url::Origin(GURL("http://foobar.com")),
MakeRequest(&wrapper));
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(
base::MakeUnique<MockLevelDBDatabaseErrorOnWrite>(&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.on_open_callback_ = reopen_loop.QuitClosure();
// Repeatedly write data to the database, to trigger enough commit errors.
while (!wrapper.encountered_error()) {
base::RunLoop put_loop;
// Every write needs to be different to make sure there actually is a
// change to commit.
value[0]++;
wrapper.set_connection_error_handler(put_loop.QuitClosure());
wrapper->Put(key, value, "source",
base::BindOnce(
[](base::Closure quit_closure, bool success) {
EXPECT_TRUE(success);
quit_closure.Run();
},
put_loop.QuitClosure()));
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(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(
base::MakeUnique<MockLevelDBDatabaseErrorOnWrite>(&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 wrapper 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(GURL("http://foobar.com")),
MakeRequest(&wrapper));
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]++;
wrapper.set_connection_error_handler(put_loop.QuitClosure());
wrapper->Put(key, value, "source",
base::BindOnce(
[](base::Closure quit_closure, bool success) {
EXPECT_TRUE(success);
quit_closure.Run();
},
put_loop.QuitClosure()));
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(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(wrapper.encountered_error());
}
} // namespace content