blob: 46598fce16522e3ea81d015286136e66416dc9b1 [file] [log] [blame]
// Copyright 2018 The Fuchsia 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 "src/modular/bin/sessionmgr/storage/story_storage.h"
#include <memory>
#include "gtest/gtest.h"
#include "peridot/lib/ledger_client/page_id.h"
#include "src/lib/fsl/vmo/strings.h"
#include "src/modular/lib/async/cpp/future.h"
#include "src/modular/lib/entity/entity_watcher_impl.h"
#include "src/modular/lib/testing/test_with_ledger.h"
using fuchsia::modular::ModuleData;
using fuchsia::modular::ModuleDataPtr;
namespace modular {
namespace {
class StoryStorageTest : public modular_testing::TestWithLedger {
protected:
std::unique_ptr<StoryStorage> CreateStorage(std::string page_id) {
return std::make_unique<StoryStorage>(ledger_client(), modular::MakePageId(page_id));
}
};
ModuleData Clone(const ModuleData& data) {
ModuleData dup;
data.Clone(&dup);
return dup;
}
TEST_F(StoryStorageTest, ReadModuleData_NonexistentModule) {
auto storage = CreateStorage("page");
bool read_done{};
std::vector<std::string> path;
path.push_back("a");
storage->ReadModuleData(path)->Then([&](ModuleDataPtr data) {
read_done = true;
ASSERT_FALSE(data);
});
RunLoopUntil([&] { return read_done; });
}
TEST_F(StoryStorageTest, ReadAllModuleData_Empty) {
auto storage = CreateStorage("page");
bool read_done{};
fidl::VectorPtr<ModuleData> all_module_data;
storage->ReadAllModuleData()->Then([&](std::vector<ModuleData> data) {
read_done = true;
all_module_data.emplace(std::move(data));
});
RunLoopUntil([&] { return read_done; });
ASSERT_TRUE(all_module_data);
EXPECT_EQ(0u, all_module_data->size());
}
TEST_F(StoryStorageTest, WriteReadModuleData) {
// Write and then read some ModuleData entries. We expect to get the same data
// back.
auto storage = CreateStorage("page");
int notification_count{0};
storage->set_on_module_data_updated([&](ModuleData) { notification_count++; });
ModuleData module_data1;
module_data1.set_module_url("url1");
module_data1.mutable_module_path()->push_back("path1");
storage->WriteModuleData(Clone(module_data1));
ModuleData module_data2;
module_data2.set_module_url("url2");
module_data2.mutable_module_path()->push_back("path2");
storage->WriteModuleData(Clone(module_data2));
// We don't need to explicitly wait on WriteModuleData() because the
// implementation: 1) serializes all storage operations and 2) guarantees the
// WriteModuleData() action is finished only once the data has been written.
ModuleData read_data1;
bool read1_done{};
storage->ReadModuleData(module_data1.module_path())->Then([&](ModuleDataPtr data) {
read1_done = true;
ASSERT_TRUE(data);
read_data1 = std::move(*data);
});
ModuleData read_data2;
bool read2_done{};
storage->ReadModuleData(module_data2.module_path())->Then([&](ModuleDataPtr data) {
read2_done = true;
ASSERT_TRUE(data);
read_data2 = std::move(*data);
});
RunLoopUntil([&] { return read1_done && read2_done; });
EXPECT_TRUE(fidl::Equals(module_data1, read_data1));
EXPECT_TRUE(fidl::Equals(module_data2, read_data2));
// Read the same data back with ReadAllModuleData().
fidl::VectorPtr<ModuleData> all_module_data;
storage->ReadAllModuleData()->Then(
[&](std::vector<ModuleData> data) { all_module_data.emplace(std::move(data)); });
RunLoopUntil([&] { return !!all_module_data; });
EXPECT_EQ(2u, all_module_data->size());
EXPECT_TRUE(fidl::Equals(module_data1, all_module_data->at(0)));
EXPECT_TRUE(fidl::Equals(module_data2, all_module_data->at(1)));
// We should get a notification every time module data is updated.
EXPECT_EQ(2, notification_count);
}
TEST_F(StoryStorageTest, UpdateModuleData) {
// Call UpdateModuleData() on a record that doesn't exist yet.
auto storage = CreateStorage("page");
// We're going to observe changes on another storage instance, which
// simulates another device.
auto other_storage = CreateStorage("page");
bool got_notification{};
ModuleData notified_module_data;
other_storage->set_on_module_data_updated([&](ModuleData data) {
got_notification = true;
notified_module_data = std::move(data);
});
std::vector<std::string> path;
path.push_back("a");
// Case 1: Don't mutate anything.
bool update_done{};
storage->UpdateModuleData(path, [](ModuleDataPtr* ptr) { EXPECT_FALSE(*ptr); })->Then([&] {
update_done = true;
});
RunLoopUntil([&] { return update_done; });
bool read_done{};
ModuleData read_data;
storage->ReadModuleData(path)->Then([&](ModuleDataPtr data) {
read_done = true;
EXPECT_FALSE(data);
});
RunLoopUntil([&] { return read_done; });
// Since nothing changed, we should not have seen a notification.
EXPECT_FALSE(got_notification);
// Case 2: Initialize an otherwise empty record.
update_done = false;
storage
->UpdateModuleData(path,
[&](ModuleDataPtr* ptr) {
EXPECT_FALSE(*ptr);
*ptr = ModuleData::New();
(*ptr)->set_module_path(path);
(*ptr)->set_module_url("foobar");
})
->Then([&] { update_done = true; });
RunLoopUntil([&] { return update_done; });
read_done = false;
storage->ReadModuleData(path)->Then([&](ModuleDataPtr data) {
read_done = true;
ASSERT_TRUE(data);
EXPECT_EQ(path, data->module_path());
EXPECT_EQ("foobar", data->module_url());
});
RunLoopUntil([&] { return read_done; });
// Now something changed, so we should see a notification.
EXPECT_TRUE(got_notification);
EXPECT_EQ("foobar", notified_module_data.module_url());
// Case 3: Leave alone an existing record.
got_notification = false;
storage->UpdateModuleData(path, [&](ModuleDataPtr* ptr) { EXPECT_TRUE(*ptr); });
read_done = false;
storage->ReadModuleData(path)->Then([&](ModuleDataPtr data) {
read_done = true;
ASSERT_TRUE(data);
EXPECT_EQ("foobar", data->module_url());
});
RunLoopUntil([&] { return read_done; });
// Now something changed, so we should see a notification.
EXPECT_FALSE(got_notification);
// Case 4: Mutate an existing record.
storage->UpdateModuleData(path, [&](ModuleDataPtr* ptr) {
EXPECT_TRUE(*ptr);
(*ptr)->set_module_url("baz");
});
read_done = false;
storage->ReadModuleData(path)->Then([&](ModuleDataPtr data) {
read_done = true;
ASSERT_TRUE(data);
EXPECT_EQ("baz", data->module_url());
});
RunLoopUntil([&] { return read_done; });
// Now something changed, so we should see a notification.
EXPECT_TRUE(got_notification);
EXPECT_EQ("baz", notified_module_data.module_url());
}
namespace {
LinkPath MakeLinkPath(const std::string& name) {
LinkPath path;
path.link_name = name;
return path;
}
} // namespace
TEST_F(StoryStorageTest, GetLink_Null) {
auto storage = CreateStorage("page");
// Default for an un-set Link is to get a "null" back.
bool get_done{};
fidl::StringPtr value;
storage->GetLinkValue(MakeLinkPath("link"))
->Then([&](StoryStorage::Status status, fidl::StringPtr v) {
EXPECT_EQ(StoryStorage::Status::OK, status);
value = v;
get_done = true;
});
RunLoopUntil([&] { return get_done; });
EXPECT_EQ("null", value);
}
TEST_F(StoryStorageTest, UpdateLinkValue) {
auto storage = CreateStorage("page");
// Let's set a value.
int mutate_count{0};
int context;
storage
->UpdateLinkValue(
MakeLinkPath("link"),
[](fidl::StringPtr* current_value) {
EXPECT_FALSE(current_value->has_value());
*current_value = "10";
},
&context)
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(StoryStorage::Status::OK, status);
++mutate_count;
});
// If we mutate again, we should see the old value.
storage
->UpdateLinkValue(
MakeLinkPath("link"),
[](fidl::StringPtr* current_value) {
EXPECT_EQ("10", *current_value);
*current_value = "20";
},
&context)
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(StoryStorage::Status::OK, status);
++mutate_count;
});
// Now let's fetch it and see the newest value.
bool get_done{};
fidl::StringPtr value;
storage->GetLinkValue(MakeLinkPath("link"))
->Then([&](StoryStorage::Status status, fidl::StringPtr v) {
EXPECT_EQ(StoryStorage::Status::OK, status);
value = v;
get_done = true;
});
RunLoopUntil([&] { return get_done; });
EXPECT_EQ(2, mutate_count);
EXPECT_EQ("20", value);
}
// Creates an entity with a valid type and data and verifies they are returned
// as expected.
TEST_F(StoryStorageTest, CreateAndReadEntity) {
auto storage = CreateStorage("page");
std::string cookie = "cookie";
std::string expected_type = "com.fuchsia.test.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool created_entity{};
storage->SetEntityData(cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
bool read_entity_type{};
storage->GetEntityType(cookie)->Then([&](StoryStorage::Status status, std::string type) {
EXPECT_EQ(type, expected_type);
read_entity_type = true;
});
RunLoopUntil([&] { return read_entity_type; });
bool read_entity_data{};
storage->GetEntityData(cookie, expected_type)
->Then([&](StoryStorage::Status status, fuchsia::mem::BufferPtr buffer) {
EXPECT_TRUE(buffer);
std::string read_data;
EXPECT_TRUE(fsl::StringFromVmo(*buffer, &read_data));
EXPECT_EQ(read_data, data_string);
read_entity_data = true;
});
RunLoopUntil([&] { return read_entity_data; });
}
// Creates an entity with a valid type and data and attempts to get the data of
// a different type.
TEST_F(StoryStorageTest, CreateAndReadEntityIncorrectType) {
auto storage = CreateStorage("page");
std::string cookie = "cookie";
std::string expected_type = "com.fuchsia.test.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool created_entity{};
storage->SetEntityData(cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
bool read_entity_data{};
storage->GetEntityData(cookie, expected_type + expected_type)
->Then([&](StoryStorage::Status status, fuchsia::mem::BufferPtr buffer) {
EXPECT_EQ(status, StoryStorage::Status::INVALID_ENTITY_TYPE);
read_entity_data = true;
});
RunLoopUntil([&] { return read_entity_data; });
}
// Creates an entity with a valid type and data and attempts to get the data of
// a different cookie.
TEST_F(StoryStorageTest, CreateAndReadEntityIncorrectCookie) {
auto storage = CreateStorage("page");
std::string cookie = "cookie";
std::string expected_type = "com.fuchsia.test.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool created_entity{};
storage->SetEntityData(cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
bool read_entity_data{};
storage->GetEntityData(cookie + cookie, expected_type)
->Then([&](StoryStorage::Status status, fuchsia::mem::BufferPtr buffer) {
EXPECT_EQ(status, StoryStorage::Status::INVALID_ENTITY_COOKIE);
read_entity_data = true;
});
RunLoopUntil([&] { return read_entity_data; });
}
// Attempts to create an entity with an empty type and verifies it fails.
TEST_F(StoryStorageTest, CreateEntityWithEmptyType) {
auto storage = CreateStorage("page");
std::string cookie = "cookie";
std::string expected_type = "";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool created_entity{};
storage->SetEntityData(cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::INVALID_ENTITY_TYPE);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
}
// Attempts to create an entity with an empty cookie and verifies it fails.
TEST_F(StoryStorageTest, CreateEntityWithEmptyCookie) {
auto storage = CreateStorage("page");
std::string cookie = "";
std::string expected_type = "com.fuchsia.test.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool created_entity{};
storage->SetEntityData(cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::INVALID_ENTITY_COOKIE);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
}
// Creates an entity and performs a second write with a different type, and
// verifies the second write fails and that the second attempted write doesn't
// corrupt the data.
TEST_F(StoryStorageTest, WriteEntityDataWithIncorrectType) {
auto storage = CreateStorage("page");
std::string cookie = "cookie";
std::string expected_type = "com.fuchsia.test.type";
std::string incorrect_type = "com.fuchsia.test.incorrect.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool created_entity{};
storage->SetEntityData(cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
bool wrote_entity{};
storage->SetEntityData(cookie, incorrect_type, fuchsia::mem::Buffer())
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::INVALID_ENTITY_TYPE);
wrote_entity = true;
});
RunLoopUntil([&] { return wrote_entity; });
// Verify that the second write didn't mess up the data or type.
bool read_entity_type{};
storage->GetEntityType(cookie)->Then([&](StoryStorage::Status status, std::string type) {
EXPECT_EQ(type, expected_type);
read_entity_type = true;
});
RunLoopUntil([&] { return read_entity_type; });
bool read_entity_data{};
storage->GetEntityData(cookie, expected_type)
->Then([&](StoryStorage::Status status, fuchsia::mem::BufferPtr buffer) {
EXPECT_TRUE(buffer);
std::string read_data;
EXPECT_TRUE(fsl::StringFromVmo(*buffer, &read_data));
EXPECT_EQ(read_data, data_string);
read_entity_data = true;
});
RunLoopUntil([&] { return read_entity_data; });
}
// Creates an entity and performs a second write with the same type, and
// verifies the data is written correctly.
TEST_F(StoryStorageTest, WriteToEntityTwice) {
auto storage = CreateStorage("page");
std::string cookie = "cookie";
std::string expected_type = "com.fuchsia.test.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
std::string second_data_string = "more_test_data";
fuchsia::mem::Buffer second_buffer;
FXL_CHECK(fsl::VmoFromString(second_data_string, &second_buffer));
bool created_entity{};
storage->SetEntityData(cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
bool wrote_entity{};
storage->SetEntityData(cookie, expected_type, std::move(second_buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
wrote_entity = true;
});
RunLoopUntil([&] { return wrote_entity; });
// Verify that the second write successfully updated the data.
bool read_entity_type{};
storage->GetEntityType(cookie)->Then([&](StoryStorage::Status status, std::string type) {
EXPECT_EQ(type, expected_type);
read_entity_type = true;
});
RunLoopUntil([&] { return read_entity_type; });
bool read_entity_data{};
storage->GetEntityData(cookie, expected_type)
->Then([&](StoryStorage::Status status, fuchsia::mem::BufferPtr buffer) {
EXPECT_TRUE(buffer);
std::string read_data;
EXPECT_TRUE(fsl::StringFromVmo(*buffer, &read_data));
EXPECT_EQ(read_data, second_data_string);
read_entity_data = true;
});
RunLoopUntil([&] { return read_entity_data; });
}
// Creates an entity with a watcher and verifies that updates to the entity are
// delivered to the watcher.
TEST_F(StoryStorageTest, WatchEntityData) {
auto storage = CreateStorage("page");
std::string expected_cookie = "cookie";
std::string expected_type = "com.fuchsia.test.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool saw_entity_update{};
bool saw_entity_update_with_no_data{};
auto watcher_impl = EntityWatcherImpl([&](std::unique_ptr<fuchsia::mem::Buffer> value) {
// Verify that the first callback is called with no data, since the
// entity data has yet to be set.
if (!value && !saw_entity_update_with_no_data) {
saw_entity_update_with_no_data = true;
return;
}
ASSERT_TRUE(value) << "Saw multiple empty entity updates.";
std::string read_data;
EXPECT_TRUE(fsl::StringFromVmo(*value, &read_data));
EXPECT_EQ(read_data, data_string);
saw_entity_update = true;
});
fuchsia::modular::EntityWatcherPtr watcher_ptr;
watcher_impl.Connect(watcher_ptr.NewRequest());
storage->WatchEntity(expected_cookie, expected_type, std::move(watcher_ptr));
bool created_entity{};
storage->SetEntityData(expected_cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
RunLoopUntil([&] { return saw_entity_update; });
EXPECT_TRUE(saw_entity_update_with_no_data);
}
// Creates an entity with multiple watchers and verifies that updates to the
// entity are delivered to all watchers.
TEST_F(StoryStorageTest, WatchEntityDataMultipleWatchers) {
auto storage = CreateStorage("page");
std::string expected_cookie = "cookie";
std::string expected_type = "com.fuchsia.test.type";
std::string data_string = "test_data";
fuchsia::mem::Buffer buffer;
FXL_CHECK(fsl::VmoFromString(data_string, &buffer));
bool saw_entity_update{};
auto watcher_impl = EntityWatcherImpl([&](std::unique_ptr<fuchsia::mem::Buffer> value) {
if (!value) {
// The first update may not contain any data, so skip it.
return;
}
std::string read_data;
EXPECT_TRUE(fsl::StringFromVmo(*value, &read_data));
EXPECT_EQ(read_data, data_string);
saw_entity_update = true;
});
fuchsia::modular::EntityWatcherPtr watcher_ptr;
watcher_impl.Connect(watcher_ptr.NewRequest());
storage->WatchEntity(expected_cookie, expected_type, std::move(watcher_ptr));
bool saw_entity_update_too{};
auto second_watcher_impl = EntityWatcherImpl([&](std::unique_ptr<fuchsia::mem::Buffer> value) {
if (!value) {
// The first update may not contain any data, so skip it.
return;
}
std::string read_data;
EXPECT_TRUE(fsl::StringFromVmo(*value, &read_data));
EXPECT_EQ(read_data, data_string);
saw_entity_update_too = true;
});
fuchsia::modular::EntityWatcherPtr second_watcher_ptr;
second_watcher_impl.Connect(second_watcher_ptr.NewRequest());
storage->WatchEntity(expected_cookie, expected_type, std::move(second_watcher_ptr));
bool created_entity{};
storage->SetEntityData(expected_cookie, expected_type, std::move(buffer))
->Then([&](StoryStorage::Status status) {
EXPECT_EQ(status, StoryStorage::Status::OK);
created_entity = true;
});
RunLoopUntil([&] { return created_entity; });
RunLoopUntil([&] { return saw_entity_update && saw_entity_update_too; });
}
// Creates a name for a given Entity cookie and verifies it is retrieved
// successfully.
TEST_F(StoryStorageTest, NameEntity) {
auto storage = CreateStorage("page");
std::string expected_cookie = "cookie";
std::string expected_name = "the best entity";
storage->SetEntityName(expected_cookie, expected_name);
bool did_get_cookie{};
storage->GetEntityCookieForName(expected_name)
->Then([&](StoryStorage::Status status, const std::string& cookie) {
EXPECT_EQ(cookie, expected_cookie);
did_get_cookie = true;
});
RunLoopUntil([&] { return did_get_cookie; });
}
} // namespace
} // namespace modular