blob: 974a028a5c8214222cb1fd1b39d4976cb8292dff [file] [log] [blame]
// Copyright 2016 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 <string.h>
#include <fuchsia/ledger/internal/cpp/fidl.h>
#include <fuchsia/sys/cpp/fidl.h>
#include <lib/callback/capture.h>
#include <lib/component/cpp/startup_context.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/fidl/cpp/synchronous_interface_ptr.h>
#include <lib/fit/function.h>
#include <lib/fsl/io/fd.h>
#include <lib/fsl/vmo/strings.h>
#include <lib/fxl/files/directory.h>
#include <lib/fxl/files/file.h>
#include <lib/gtest/real_loop_fixture.h>
#include <lib/svc/cpp/services.h>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "peridot/bin/ledger/app/serialization_version.h"
#include "peridot/bin/ledger/fidl/include/types.h"
#include "peridot/bin/ledger/filesystem/detached_path.h"
#include "peridot/bin/ledger/filesystem/directory_reader.h"
#include "peridot/bin/ledger/testing/cloud_provider/fake_cloud_provider.h"
#include "peridot/bin/ledger/testing/cloud_provider/types.h"
#include "peridot/lib/scoped_tmpfs/scoped_tmpfs.h"
namespace test {
namespace e2e_local {
namespace {
using ::testing::ElementsAre;
template <class A>
bool Equals(const fidl::VectorPtr<uint8_t>& a1, const A& a2) {
if (a1->size() != a2.size())
return false;
return memcmp(a1->data(), a2.data(), a1->size()) == 0;
}
fidl::VectorPtr<uint8_t> TestArray() {
std::string value = "value";
fidl::VectorPtr<uint8_t> result(value.size());
memcpy(&result->at(0), &value[0], value.size());
return result;
}
class LedgerEndToEndTest : public gtest::RealLoopFixture {
public:
LedgerEndToEndTest()
: startup_context_(component::StartupContext::CreateFromStartupInfo()) {}
~LedgerEndToEndTest() override {}
protected:
void Init(std::vector<std::string> additional_args) {
component::Services child_services;
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = "fuchsia-pkg://fuchsia.com/ledger#meta/ledger.cmx";
launch_info.directory_request = child_services.NewRequest();
launch_info.arguments.push_back("--disable_reporting");
for (auto& additional_arg : additional_args) {
launch_info.arguments.push_back(additional_arg);
}
startup_context()->launcher()->CreateComponent(
std::move(launch_info), ledger_controller_.NewRequest());
ledger_controller_.set_error_handler([this](zx_status_t status) {
for (const auto& callback : ledger_shutdown_callbacks_) {
callback();
}
});
ledger_repository_factory_.set_error_handler([](zx_status_t status) {
if (status != ZX_ERR_PEER_CLOSED) {
ADD_FAILURE() << "Ledger repository error: " << status;
}
});
child_services.ConnectToService(ledger_repository_factory_.NewRequest());
child_services.ConnectToService(controller_.NewRequest());
}
void RegisterShutdownCallback(fit::function<void()> callback) {
ledger_shutdown_callbacks_.push_back(std::move(callback));
}
component::StartupContext* startup_context() {
return startup_context_.get();
}
private:
fuchsia::sys::ComponentControllerPtr ledger_controller_;
std::vector<fit::function<void()>> ledger_shutdown_callbacks_;
std::unique_ptr<component::StartupContext> startup_context_;
protected:
ledger_internal::LedgerRepositoryFactoryPtr ledger_repository_factory_;
fidl::SynchronousInterfacePtr<ledger::Ledger> ledger_;
fidl::SynchronousInterfacePtr<ledger_internal::LedgerController> controller_;
};
TEST_F(LedgerEndToEndTest, PutAndGet) {
Init({});
ledger::Status status;
fidl::SynchronousInterfacePtr<ledger_internal::LedgerRepository>
ledger_repository;
scoped_tmpfs::ScopedTmpFS tmpfs;
ledger_repository_factory_->GetRepository(
fsl::CloneChannelFromFileDescriptor(tmpfs.root_fd()), nullptr, "",
ledger_repository.NewRequest());
ledger_repository->GetLedger(TestArray(), ledger_.NewRequest());
ASSERT_EQ(ZX_OK, ledger_repository->Sync());
fidl::SynchronousInterfacePtr<ledger::Page> page;
ledger_->GetRootPage(page.NewRequest(), &status);
ASSERT_EQ(ledger::Status::OK, status);
page->Put(TestArray(), TestArray(), &status);
EXPECT_EQ(ledger::Status::OK, status);
fidl::SynchronousInterfacePtr<ledger::PageSnapshot> snapshot;
page->GetSnapshot(snapshot.NewRequest(), fidl::VectorPtr<uint8_t>::New(0),
nullptr, &status);
EXPECT_EQ(ledger::Status::OK, status);
fuchsia::mem::BufferPtr value;
snapshot->Get(TestArray(), &status, &value);
EXPECT_EQ(ledger::Status::OK, status);
std::string value_as_string;
EXPECT_TRUE(fsl::StringFromVmo(*value, &value_as_string));
EXPECT_TRUE(Equals(TestArray(), value_as_string));
}
TEST_F(LedgerEndToEndTest, Terminate) {
Init({});
bool called = false;
RegisterShutdownCallback([this, &called] {
called = true;
QuitLoop();
});
controller_->Terminate();
RunLoop();
EXPECT_TRUE(called);
}
// Verifies the cloud erase recovery in case of a cloud that was erased before
// startup.
//
// Expected behavior: Ledger disconnects the clients and the local state is
// cleared.
TEST_F(LedgerEndToEndTest, CloudEraseRecoveryOnInitialCheck) {
Init({});
bool ledger_shut_down = false;
RegisterShutdownCallback([&ledger_shut_down] { ledger_shut_down = true; });
ledger_internal::LedgerRepositoryPtr ledger_repository;
scoped_tmpfs::ScopedTmpFS tmpfs;
const std::string content_path = ledger::kSerializationVersion.ToString();
const std::string deletion_sentinel_path = content_path + "/sentinel";
ASSERT_TRUE(files::CreateDirectoryAt(tmpfs.root_fd(), content_path));
ASSERT_TRUE(
files::WriteFileAt(tmpfs.root_fd(), deletion_sentinel_path, "", 0));
ASSERT_TRUE(files::IsFileAt(tmpfs.root_fd(), deletion_sentinel_path));
// Write a fingerprint file, so that Ledger will check if it is still in the
// cloud device set.
const std::string fingerprint_path = content_path + "/fingerprint";
const std::string fingerprint = "bazinga";
ASSERT_TRUE(files::WriteFileAt(tmpfs.root_fd(), fingerprint_path,
fingerprint.c_str(), fingerprint.size()));
// Create a cloud provider configured to trigger the cloude erase recovery on
// initial check.
auto cloud_provider =
ledger::FakeCloudProvider::Builder()
.SetCloudEraseOnCheck(ledger::CloudEraseOnCheck::YES)
.Build();
cloud_provider::CloudProviderPtr cloud_provider_ptr;
fidl::Binding<cloud_provider::CloudProvider> cloud_provider_binding(
cloud_provider.get(), cloud_provider_ptr.NewRequest());
ledger_repository_factory_->GetRepository(
fsl::CloneChannelFromFileDescriptor(tmpfs.root_fd()),
std::move(cloud_provider_ptr), "user_id", ledger_repository.NewRequest());
bool repo_disconnected = false;
ledger_repository.set_error_handler(
[&repo_disconnected](zx_status_t status) { repo_disconnected = true; });
// Run the message loop until Ledger clears the repo directory and disconnects
// the client.
RunLoopUntil([&] {
return !files::IsFileAt(tmpfs.root_fd(), deletion_sentinel_path) &&
repo_disconnected;
});
EXPECT_FALSE(files::IsFileAt(tmpfs.root_fd(), deletion_sentinel_path));
EXPECT_TRUE(repo_disconnected);
// Make sure all the contents are deleted. Only the staging directory should
// be present.
std::vector<std::string> directory_entries;
auto on_next_directory_entry = [&](fxl::StringView entry) {
directory_entries.push_back(entry.ToString());
return true;
};
EXPECT_TRUE(ledger::GetDirectoryEntries(ledger::DetachedPath(tmpfs.root_fd()),
on_next_directory_entry));
EXPECT_THAT(directory_entries, ElementsAre("staging"));
// Verify that the Ledger app didn't crash.
EXPECT_FALSE(ledger_shut_down);
}
// Verifies the cloud erase recovery in case of a cloud that is erased while
// Ledger is connected to it.
//
// Expected behavior: Ledger disconnects the clients and the local state is
// cleared.
TEST_F(LedgerEndToEndTest, CloudEraseRecoveryFromTheWatcher) {
Init({});
bool ledger_shut_down = false;
RegisterShutdownCallback([&ledger_shut_down] { ledger_shut_down = true; });
ledger_internal::LedgerRepositoryPtr ledger_repository;
scoped_tmpfs::ScopedTmpFS tmpfs;
const std::string content_path = ledger::kSerializationVersion.ToString();
const std::string deletion_sentinel_path = content_path + "/sentinel";
ASSERT_TRUE(files::CreateDirectoryAt(tmpfs.root_fd(), content_path));
ASSERT_TRUE(
files::WriteFileAt(tmpfs.root_fd(), deletion_sentinel_path, "", 0));
ASSERT_TRUE(files::IsFileAt(tmpfs.root_fd(), deletion_sentinel_path));
// Create a cloud provider configured to trigger the cloud erase recovery
// while Ledger is connected.
auto cloud_provider =
ledger::FakeCloudProvider::Builder()
.SetCloudEraseFromWatcher(ledger::CloudEraseFromWatcher::YES)
.Build();
cloud_provider::CloudProviderPtr cloud_provider_ptr;
fidl::Binding<cloud_provider::CloudProvider> cloud_provider_binding(
cloud_provider.get(), cloud_provider_ptr.NewRequest());
ledger_repository_factory_->GetRepository(
fsl::CloneChannelFromFileDescriptor(tmpfs.root_fd()),
std::move(cloud_provider_ptr), "user_id", ledger_repository.NewRequest());
bool repo_disconnected = false;
ledger_repository.set_error_handler(
[&repo_disconnected](zx_status_t status) { repo_disconnected = true; });
// Run the message loop until Ledger clears the repo directory and disconnects
// the client.
RunLoopUntil([&] {
return !files::IsFileAt(tmpfs.root_fd(), deletion_sentinel_path) &&
repo_disconnected;
});
EXPECT_FALSE(files::IsFileAt(tmpfs.root_fd(), deletion_sentinel_path));
EXPECT_TRUE(repo_disconnected);
// Verify that the Ledger app didn't crash.
EXPECT_FALSE(ledger_shut_down);
}
// Verifies that Ledger instance continues to work even if the cloud provider
// goes away (for example, because it crashes).
//
// In the future, we need to also be able to reconnect/request a new cloud
// provider, see LE-567.
TEST_F(LedgerEndToEndTest, HandleCloudProviderDisconnectBeforePageInit) {
Init({});
bool ledger_app_shut_down = false;
RegisterShutdownCallback(
[&ledger_app_shut_down] { ledger_app_shut_down = true; });
ledger::Status status;
scoped_tmpfs::ScopedTmpFS tmpfs;
cloud_provider::CloudProviderPtr cloud_provider_ptr;
fidl::SynchronousInterfacePtr<ledger_internal::LedgerRepository>
ledger_repository;
ledger::FakeCloudProvider cloud_provider;
fidl::Binding<cloud_provider::CloudProvider> cloud_provider_binding(
&cloud_provider, cloud_provider_ptr.NewRequest());
ledger_repository_factory_->GetRepository(
fsl::CloneChannelFromFileDescriptor(tmpfs.root_fd()),
std::move(cloud_provider_ptr), "user_id", ledger_repository.NewRequest());
ledger_repository->GetLedger(TestArray(), ledger_.NewRequest());
ASSERT_EQ(ZX_OK, ledger_repository->Sync());
// Close the cloud provider channel.
cloud_provider_binding.Unbind();
// Write and read some data to verify that Ledger still works.
fidl::SynchronousInterfacePtr<ledger::Page> page;
status = ledger::Status::INTERNAL_ERROR;
ledger_->GetPage(nullptr, page.NewRequest(), &status);
ASSERT_EQ(ledger::Status::OK, status);
status = ledger::Status::INTERNAL_ERROR;
page->Put(TestArray(), TestArray(), &status);
EXPECT_EQ(ledger::Status::OK, status);
fidl::SynchronousInterfacePtr<ledger::PageSnapshot> snapshot;
status = ledger::Status::INTERNAL_ERROR;
page->GetSnapshot(snapshot.NewRequest(), fidl::VectorPtr<uint8_t>::New(0),
nullptr, &status);
EXPECT_EQ(ledger::Status::OK, status);
fuchsia::mem::BufferPtr value;
status = ledger::Status::INTERNAL_ERROR;
snapshot->Get(TestArray(), &status, &value);
ASSERT_EQ(ledger::Status::OK, status);
std::string value_as_string;
EXPECT_TRUE(fsl::StringFromVmo(*value, &value_as_string));
EXPECT_TRUE(Equals(TestArray(), value_as_string));
// Verify that the Ledger app didn't crash or shut down.
EXPECT_TRUE(ledger_repository);
EXPECT_FALSE(ledger_app_shut_down);
}
TEST_F(LedgerEndToEndTest, HandleCloudProviderDisconnectBetweenReadAndWrite) {
Init({});
bool ledger_app_shut_down = false;
RegisterShutdownCallback(
[&ledger_app_shut_down] { ledger_app_shut_down = true; });
ledger::Status status;
scoped_tmpfs::ScopedTmpFS tmpfs;
cloud_provider::CloudProviderPtr cloud_provider_ptr;
fidl::SynchronousInterfacePtr<ledger_internal::LedgerRepository>
ledger_repository;
ledger::FakeCloudProvider cloud_provider;
fidl::Binding<cloud_provider::CloudProvider> cloud_provider_binding(
&cloud_provider, cloud_provider_ptr.NewRequest());
ledger_repository_factory_->GetRepository(
fsl::CloneChannelFromFileDescriptor(tmpfs.root_fd()),
std::move(cloud_provider_ptr), "user_id", ledger_repository.NewRequest());
ledger_repository->GetLedger(TestArray(), ledger_.NewRequest());
ASSERT_EQ(ZX_OK, ledger_repository->Sync());
// Write some data.
fidl::SynchronousInterfacePtr<ledger::Page> page;
status = ledger::Status::INTERNAL_ERROR;
ledger_->GetPage(nullptr, page.NewRequest(), &status);
ASSERT_EQ(ledger::Status::OK, status);
status = ledger::Status::INTERNAL_ERROR;
page->Put(TestArray(), TestArray(), &status);
EXPECT_EQ(ledger::Status::OK, status);
// Close the cloud provider channel.
cloud_provider_binding.Unbind();
// Read the data back.
fidl::SynchronousInterfacePtr<ledger::PageSnapshot> snapshot;
status = ledger::Status::INTERNAL_ERROR;
page->GetSnapshot(snapshot.NewRequest(), fidl::VectorPtr<uint8_t>::New(0),
nullptr, &status);
EXPECT_EQ(ledger::Status::OK, status);
fuchsia::mem::BufferPtr value;
status = ledger::Status::INTERNAL_ERROR;
snapshot->Get(TestArray(), &status, &value);
ASSERT_EQ(ledger::Status::OK, status);
std::string value_as_string;
EXPECT_TRUE(fsl::StringFromVmo(*value, &value_as_string));
EXPECT_TRUE(Equals(TestArray(), value_as_string));
// Verify that the Ledger app didn't crash or shut down.
EXPECT_TRUE(ledger_repository);
EXPECT_FALSE(ledger_app_shut_down);
}
} // namespace
} // namespace e2e_local
} // namespace test