blob: 42445174c8864093a26663b8d2a026e18986b004 [file] [log] [blame]
// Copyright 2017 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 "peridot/bin/story_runner/link_impl.h"
#include <fuchsia/cpp/modular.h>
#include "gtest/gtest.h"
#include "lib/async/cpp/operation.h"
#include "lib/fidl/cpp/array.h"
#include "peridot/lib/fidl/array_to_string.h"
#include "peridot/lib/fidl/clone.h"
#include "peridot/lib/fidl/json_xdr.h"
#include "peridot/lib/ledger_client/ledger_client.h"
#include "peridot/lib/ledger_client/page_client.h"
#include "peridot/lib/ledger_client/page_id.h"
#include "peridot/lib/ledger_client/storage.h"
#include "peridot/lib/rapidjson/rapidjson.h"
#include "peridot/lib/testing/test_with_ledger.h"
#include "peridot/public/lib/entity/cpp/json.h"
namespace modular {
namespace {
const std::string kInitialLinkValue = "{}";
} // namespace
// Defined in incremental_link.cc.
void XdrLinkChange(XdrContext* const xdr, LinkChange* const data);
namespace {
LinkPath GetTestLinkPath() {
LinkPath link_path;
link_path.module_path.push_back("root");
link_path.module_path.push_back("photos");
link_path.link_name = "theLinkName";
return link_path;
}
// TODO(mesch): Duplicated from ledger_client.cc.
bool HasPrefix(const std::string& value, const std::string& prefix) {
if (value.size() < prefix.size()) {
return false;
}
for (size_t i = 0; i < prefix.size(); ++i) {
if (value[i] != prefix[i]) {
return false;
}
}
return true;
}
class PageClientPeer : modular::PageClient {
public:
PageClientPeer(LedgerClient* const ledger_client,
LedgerPageId page_id,
std::string expected_prefix)
: PageClient("PageClientPeer", ledger_client, std::move(page_id)),
expected_prefix_(std::move(expected_prefix)) {}
void OnPageChange(const std::string& key, const std::string& value) {
EXPECT_TRUE(HasPrefix(key, expected_prefix_))
<< " key=" << key << " expected_prefix=" << expected_prefix_;
changes.push_back(std::make_pair(key, value));
EXPECT_TRUE(XdrRead(value, &last_change, XdrLinkChange))
<< key << " " << value;
FXL_LOG(INFO) << "PageChange " << key << " = " << value;
};
std::vector<std::pair<std::string, std::string>> changes;
LinkChangePtr last_change;
private:
std::string expected_prefix_;
};
class LinkImplTestBase : public testing::TestWithLedger, modular::LinkWatcher {
public:
LinkImplTestBase() : watcher_binding_(this) {}
virtual CreateLinkInfoPtr GetCreateLinkInfo() = 0;
void SetUp() override {
TestWithLedger::SetUp();
OperationBase::set_observer([this](const char* const operation_name) {
++operations_[operation_name];
});
auto page_id = MakePageId("0123456789123456");
auto link_path = GetTestLinkPath();
auto create_link_info = GetCreateLinkInfo();
link_impl_ =
std::make_unique<LinkImpl>(ledger_client(), CloneStruct(page_id),
link_path, std::move(create_link_info));
link_impl_->Connect(link_.NewRequest(), LinkImpl::ConnectionType::Primary);
ledger_client_peer_ = ledger_client()->GetLedgerClientPeer();
page_client_peer_ = std::make_unique<PageClientPeer>(
ledger_client_peer_.get(), CloneStruct(page_id),
MakeLinkKey(link_path));
}
void TearDown() override {
if (watcher_binding_.is_bound()) {
watcher_binding_.Unbind();
}
link_impl_.reset();
link_.Unbind();
page_client_peer_.reset();
ledger_client_peer_.reset();
OperationBase::set_observer(nullptr);
TestWithLedger::TearDown();
}
int ledger_change_count() const { return page_client_peer_->changes.size(); }
LinkChangePtr& last_change() { return page_client_peer_->last_change; }
void ExpectOneCall(const std::string& operation_name) {
EXPECT_EQ(1u, operations_.count(operation_name))
<< operation_name << " was not called.";
EXPECT_EQ(1, operations_[operation_name]) << operation_name;
operations_.erase(operation_name);
}
void ExpectAtLeastCalls(const std::string& operation_name, int n) {
EXPECT_EQ(1u, operations_.count(operation_name))
<< operation_name << " was not called.";
EXPECT_LE(n, operations_[operation_name]) << operation_name;
operations_.erase(operation_name);
}
void ExpectCalls(const std::string& operation_name, int n) {
EXPECT_EQ(1u, operations_.count(operation_name))
<< operation_name << " was not called.";
EXPECT_EQ(n, operations_[operation_name]) << operation_name;
operations_.erase(operation_name);
}
void ExpectNoOtherCalls() {
EXPECT_TRUE(operations_.empty());
for (const auto& c : operations_) {
FXL_LOG(INFO) << " Unexpected call: " << c.first;
}
}
void ClearCalls() { operations_.clear(); }
void Notify(fidl::StringPtr json) override {
step_++;
last_json_notify_ = json;
continue_();
};
std::unique_ptr<LinkImpl> link_impl_;
LinkPtr link_;
std::unique_ptr<LedgerClient> ledger_client_peer_;
std::unique_ptr<PageClientPeer> page_client_peer_;
fidl::Binding<modular::LinkWatcher> watcher_binding_;
int step_{};
std::string last_json_notify_;
std::function<void()> continue_;
std::map<std::string, int> operations_;
};
class LinkImplTest : public LinkImplTestBase {
public:
LinkImplTest() = default;
~LinkImplTest() = default;
CreateLinkInfoPtr GetCreateLinkInfo() override {
auto create_link_info = CreateLinkInfo::New();
create_link_info->permissions = LinkPermissions::READ_WRITE;
create_link_info->initial_data = kInitialLinkValue;
// |create_link_info->allowed_types| is already null.
return create_link_info;
}
};
TEST_F(LinkImplTest, Constructor) {
continue_ = [this] { EXPECT_LE(step_, 1); };
link_->WatchAll(watcher_binding_.NewBinding());
bool synced{};
link_->Sync([&synced] { synced = true; });
EXPECT_TRUE(RunLoopUntilWithTimeout([&synced] { return synced; }));
EXPECT_EQ(1, ledger_change_count());
EXPECT_EQ(kInitialLinkValue, last_json_notify_);
ExpectOneCall("LinkImpl::ReloadCall");
ExpectOneCall("ReadAllDataCall");
// All numbers for |IncrementalChangeCall| are "at least" because PageClient
// will make a callback once per write, effectively doubling the number of
// calls. However, |LinkImpl::OnPageChange| puts those requests on an
// OperationQueue, so each request may or may not have run by the time Sync()
// returns.
ExpectAtLeastCalls("LinkImpl::IncrementalChangeCall", ledger_change_count());
ExpectCalls("LinkImpl::IncrementalWriteCall", ledger_change_count());
ExpectCalls("WriteDataCall", ledger_change_count());
ExpectOneCall("LinkImpl::WatchCall");
ExpectOneCall("SyncCall");
ExpectNoOtherCalls();
}
TEST_F(LinkImplTest, Set) {
continue_ = [this] { EXPECT_LE(step_, 2); };
link_->WatchAll(watcher_binding_.NewBinding());
link_->Set(nullptr, "{ \"value\": 7 }");
bool synced{};
link_->Sync([&synced] { synced = true; });
EXPECT_TRUE(RunLoopUntilWithTimeout([&synced] { return synced; }));
EXPECT_EQ(2, ledger_change_count());
// Calls from constructor and setup.
ExpectOneCall("LinkImpl::ReloadCall");
ExpectOneCall("ReadAllDataCall");
ExpectOneCall("LinkImpl::WatchCall");
// Calls from Set().
ExpectAtLeastCalls("LinkImpl::IncrementalChangeCall", ledger_change_count());
ExpectCalls("LinkImpl::IncrementalWriteCall", ledger_change_count());
ExpectCalls("WriteDataCall", ledger_change_count());
ExpectOneCall("SyncCall");
ExpectNoOtherCalls();
EXPECT_EQ("{\"value\":7}", last_json_notify_);
}
TEST_F(LinkImplTest, Update) {
continue_ = [this] { EXPECT_LE(step_, 3); };
link_->WatchAll(watcher_binding_.NewBinding());
link_->Set(nullptr, "{ \"value\": 8 }");
link_->UpdateObject(nullptr, "{ \"value\": 50 }");
bool synced{};
link_->Sync([&synced] { synced = true; });
EXPECT_TRUE(RunLoopUntilWithTimeout([&synced] { return synced; }));
EXPECT_EQ(3, ledger_change_count());
ExpectAtLeastCalls("LinkImpl::IncrementalChangeCall", ledger_change_count());
ExpectCalls("LinkImpl::IncrementalWriteCall", ledger_change_count());
ExpectCalls("WriteDataCall", ledger_change_count());
EXPECT_EQ("{\"value\":50}", last_change()->json);
EXPECT_EQ("{\"value\":50}", last_json_notify_);
}
TEST_F(LinkImplTest, UpdateNewKey) {
continue_ = [this] { EXPECT_LE(step_, 3); };
link_->WatchAll(watcher_binding_.NewBinding());
link_->Set(nullptr, "{ \"value\": 9 }");
link_->UpdateObject(nullptr, "{ \"century\": 100 }");
bool synced{};
link_->Sync([&synced] { synced = true; });
EXPECT_TRUE(RunLoopUntilWithTimeout([&synced] { return synced; }));
EXPECT_EQ(3, ledger_change_count());
ExpectAtLeastCalls("LinkImpl::IncrementalChangeCall", ledger_change_count());
ExpectCalls("LinkImpl::IncrementalWriteCall", ledger_change_count());
ExpectCalls("WriteDataCall", ledger_change_count());
EXPECT_EQ("{\"century\":100}", last_change()->json);
EXPECT_EQ("{\"value\":9,\"century\":100}", last_json_notify_);
}
TEST_F(LinkImplTest, Erase) {
continue_ = [this] { EXPECT_LE(step_, 3); };
link_->WatchAll(watcher_binding_.NewBinding());
link_->Set(nullptr, "{ \"value\": 4 }");
fidl::VectorPtr<fidl::StringPtr> segments;
segments.push_back("value");
link_->Erase(std::move(segments));
bool synced{};
link_->Sync([&synced] { synced = true; });
EXPECT_TRUE(RunLoopUntilWithTimeout([&synced] { return synced; }));
EXPECT_EQ(3, ledger_change_count());
ExpectAtLeastCalls("LinkImpl::IncrementalChangeCall", ledger_change_count());
ExpectCalls("LinkImpl::IncrementalWriteCall", ledger_change_count());
ExpectCalls("WriteDataCall", ledger_change_count());
EXPECT_TRUE(last_change()->json.is_null());
EXPECT_EQ("{}", last_json_notify_);
}
TEST_F(LinkImplTest, SetEntity) {
continue_ = [this] { EXPECT_LE(step_, 4); };
const char entity_ref[] = "entertaining-entity";
const std::string entity_ref_json = EntityReferenceToJson(entity_ref);
link_->WatchAll(watcher_binding_.NewBinding());
link_->SetEntity(entity_ref);
bool synced{};
link_->Sync([&synced] { synced = true; });
EXPECT_TRUE(RunLoopUntilWithTimeout([&synced] { return synced; }));
EXPECT_EQ(2, ledger_change_count());
ExpectAtLeastCalls("LinkImpl::IncrementalChangeCall", ledger_change_count());
ExpectCalls("LinkImpl::IncrementalWriteCall", ledger_change_count());
ExpectCalls("WriteDataCall", ledger_change_count());
// SetEntity() delegates to Set(), which was tested above, so don't
// repeat those tests here.
EXPECT_EQ(entity_ref_json, last_json_notify_);
bool done{};
link_->GetEntity([entity_ref, &done](const fidl::StringPtr value) {
EXPECT_EQ(entity_ref, value);
done = true;
});
EXPECT_TRUE(RunLoopUntilWithTimeout([&done] { return done; }));
}
class LinkImplReadOnlyTest : public LinkImplTestBase {
public:
LinkImplReadOnlyTest() = default;
~LinkImplReadOnlyTest() = default;
CreateLinkInfoPtr GetCreateLinkInfo() override {
auto create_link_info = CreateLinkInfo::New();
create_link_info->permissions = LinkPermissions::READ_ONLY_FOR_OTHERS;
// create_link_info->initial_data| is left null.
// |create_link_info->allowed_types| is already null.
return create_link_info;
}
LinkPtr link2_;
};
// In contrast to the Constructor test for LinkImplTest,
// |create_link_info->initial_data| was set to null for this test, so nothing
// was written to the link by default.
TEST_F(LinkImplReadOnlyTest, Constructor) {
continue_ = [this] { EXPECT_LE(step_, 1); };
link_->WatchAll(watcher_binding_.NewBinding());
bool synced{};
link_->Sync([&synced] { synced = true; });
EXPECT_TRUE(RunLoopUntilWithTimeout([&synced] { return synced; }));
EXPECT_EQ(0, ledger_change_count());
EXPECT_EQ("null", last_json_notify_);
ExpectOneCall("LinkImpl::ReloadCall");
ExpectOneCall("ReadAllDataCall");
// All numbers for |IncrementalChangeCall| are "at least" because PageClient
// will make a callback once per write, effectively doubling the number of
// calls. However, |LinkImpl::OnPageChange| puts those requests on an
// OperationQueue, so each request may or may not have run by the time Sync()
// returns.
ExpectOneCall("LinkImpl::WatchCall");
ExpectOneCall("SyncCall");
ExpectNoOtherCalls();
}
TEST_F(LinkImplReadOnlyTest, Set) {
continue_ = [this] { EXPECT_LE(step_, 2); };
link_->WatchAll(watcher_binding_.NewBinding());
link_impl_->Connect(link2_.NewRequest(), LinkImpl::ConnectionType::Secondary);
// This should be a no-op because |link2_| is read-only.
link2_->Set(nullptr, "\"from_link2\"");
// Writing from_link2 silently fails, so we need to write another value
// so we have a condition against which we can detect the end of the test.
link_->Set(nullptr, "\"from_link\"");
EXPECT_TRUE(RunLoopUntilWithTimeout([this] {
return "\"from_link\"" == last_json_notify_ && ledger_change_count() == 1;
}));
}
class LinkImplNullInitTest : public LinkImplTestBase {
public:
LinkImplNullInitTest() = default;
~LinkImplNullInitTest() = default;
CreateLinkInfoPtr GetCreateLinkInfo() override { return CreateLinkInfoPtr(); }
};
TEST_F(LinkImplNullInitTest, Set) {
// Even though we only write one value, we get two notifications, one
// for the initial value of null and one for the Set() call below.
continue_ = [this] { EXPECT_LE(step_, 2); };
link_->WatchAll(watcher_binding_.NewBinding());
link_->Set(nullptr, "\"from_link\"");
EXPECT_TRUE(RunLoopUntilWithTimeout([this] {
return "\"from_link\"" == last_json_notify_ && ledger_change_count() == 1;
}));
}
// TODO(jimbe) Still many tests to be written, including:
//
// * testing that setting a schema prevents WriteLinkData from being called if
// the json is bad,
//
// * Specific behavior of LinkWatcher notification (Watch() not called for own
// changes, Watch() and WatchAll() only called for changes that really occur,
// and only once.
} // namespace
} // namespace modular