blob: 7337588347cd20287f99f2354f538d2c8b547222 [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 "peridot/bin/sessionmgr/puppet_master/command_runners/add_mod_command_runner.h"
#include <lib/fidl/cpp/clone.h>
#include <lib/fsl/types/type_converters.h>
#include <lib/fsl/vmo/strings.h>
#include <lib/fxl/type_converter.h>
#include <lib/gtest/test_loop_fixture.h>
#include "gtest/gtest.h"
#include "peridot/lib/ledger_client/page_id.h"
#include "peridot/lib/testing/entity_resolver_fake.h"
#include "peridot/lib/testing/module_resolver_fake.h"
#include "peridot/lib/testing/test_with_session_storage.h"
namespace modular {
namespace {
fuchsia::modular::IntentFilter MakeIntentFilter(
std::string action,
std::vector<fuchsia::modular::ParameterConstraint> param_constraints) {
fuchsia::modular::IntentFilter f;
f.action = action;
f.parameter_constraints = param_constraints;
return f;
}
class AddModCommandRunnerTest : public testing::TestWithSessionStorage {
public:
void SetUp() override {
testing::TestWithSessionStorage::SetUp();
session_storage_ = MakeSessionStorage("page");
story_id_ = CreateStory(session_storage_.get());
story_storage_ = GetStoryStorage(session_storage_.get(), story_id_);
fake_module_resolver_->Connect(module_resolver_.NewRequest());
fake_entity_resolver_->Connect(entity_resolver_.NewRequest());
runner_ = MakeRunner(module_resolver_.get(), entity_resolver_.get());
}
protected:
// This method compares intents field by field, where fuchsia::mem::Buffers
// are compared via their contents.
bool AreIntentsEqual(const fuchsia::modular::Intent& old_intent,
const fuchsia::modular::Intent& new_intent) {
if (old_intent.handler != new_intent.handler) {
return false;
}
if (old_intent.action != new_intent.action) {
return false;
}
std::map<fidl::StringPtr, const fuchsia::modular::IntentParameterData*>
old_params;
if (old_intent.parameters) {
for (const auto& entry : *old_intent.parameters) {
old_params[entry.name] = &entry.data;
}
}
std::map<fidl::StringPtr, const fuchsia::modular::IntentParameterData*>
new_params;
if (new_intent.parameters) {
for (const auto& entry : *new_intent.parameters) {
new_params[entry.name] = &entry.data;
}
}
if (new_params.size() != old_params.size()) {
return false;
}
for (const auto& entry : new_params) {
const auto& name = entry.first;
if (old_params.count(name) == 0) {
return false;
}
const auto& new_param = *entry.second;
const auto& old_param = *old_params[name];
// If a parameter type changed, or a link mapping changed, we
// need to relaunch.
if (old_param.Which() != new_param.Which()) {
return false;
}
switch (old_param.Which()) {
case fuchsia::modular::IntentParameterData::Tag::kEntityType:
if (old_param.entity_type() != new_param.entity_type()) {
return false;
}
break;
case fuchsia::modular::IntentParameterData::Tag::kEntityReference:
if (old_param.entity_reference() != new_param.entity_reference()) {
return false;
}
break;
case fuchsia::modular::IntentParameterData::Tag::kJson: {
std::string old_string;
std::string new_string;
if (old_param.json().size == 0 && new_param.json().size == 0) {
break;
}
FXL_CHECK(fsl::StringFromVmo(old_param.json(), &old_string));
FXL_CHECK(fsl::StringFromVmo(new_param.json(), &new_string));
if (old_string != new_string) {
return false;
}
break;
}
case fuchsia::modular::IntentParameterData::Tag::Invalid:
break;
}
}
return true;
}
std::unique_ptr<AddModCommandRunner> MakeRunner(
fuchsia::modular::ModuleResolver* const module_resolver,
fuchsia::modular::EntityResolver* const entity_resolver) {
return std::make_unique<AddModCommandRunner>(module_resolver,
entity_resolver);
}
fuchsia::modular::StoryCommand MakeAddModCommand(
const std::string& mod_name, const std::string& parent_mod_name,
float surface_emphasis, const fuchsia::modular::Intent& intent) {
fuchsia::modular::AddMod add_mod;
add_mod.mod_name = {mod_name};
if (!parent_mod_name.empty()) {
add_mod.surface_parent_mod_name.reset({parent_mod_name});
}
add_mod.surface_relation.emphasis = surface_emphasis;
intent.Clone(&add_mod.intent);
fuchsia::modular::StoryCommand command;
command.set_add_mod(std::move(add_mod));
return command;
}
fuchsia::modular::Intent CreateEmptyIntent(const std::string& action,
const std::string& handler = "") {
fuchsia::modular::Intent intent;
intent.action = "intent_action";
if (!handler.empty()) {
intent.handler = "mod_url";
}
return intent;
}
void AddEntityRefParameter(fuchsia::modular::Intent* intent,
const std::string& name,
const std::string& reference) {
fuchsia::modular::IntentParameter parameter;
parameter.name = name;
parameter.data.set_entity_reference(reference);
intent->parameters.push_back(std::move(parameter));
}
void AddEntityTypeParameter(fuchsia::modular::Intent* intent,
const std::string& name,
std::vector<std::string> types) {
fuchsia::modular::IntentParameter parameter;
parameter.name = name;
parameter.data.set_entity_type(types);
intent->parameters.push_back(std::move(parameter));
}
void AddParameterWithoutName(fuchsia::modular::Intent* intent) {
fuchsia::modular::IntentParameter parameter;
fsl::SizedVmo vmo;
FXL_CHECK(fsl::VmoFromString("10", &vmo));
parameter.data.set_json(std::move(vmo).ToTransport());
intent->parameters.push_back(std::move(parameter));
}
void AddJsonParameter(fuchsia::modular::Intent* intent,
const std::string& name, const std::string& json) {
fuchsia::modular::IntentParameter parameter;
parameter.name = name;
fsl::SizedVmo vmo;
FXL_CHECK(fsl::VmoFromString(json, &vmo));
parameter.data.set_json(std::move(vmo).ToTransport());
intent->parameters.push_back(std::move(parameter));
}
void AddInvalidParameter(fuchsia::modular::Intent* intent,
const std::string& name) {
// This parameter has no data union field set, hence it's invalid.
fuchsia::modular::IntentParameterData data;
fuchsia::modular::IntentParameter parameter;
parameter.name = name;
parameter.data = std::move(data);
intent->parameters.push_back(std::move(parameter));
}
// Initializes a parent mod for the mod created during the test. The goal of
// this mod is to test parameters of type link_name and as the
// surface_relation_parent_mod.
void InitParentMod(const std::string& mod_name, const std::string& param_name,
const std::string& param_value,
const std::string& link_path_name) {
fuchsia::modular::ModuleData module_data;
module_data.module_path.push_back(mod_name);
module_data.intent = fuchsia::modular::Intent::New();
AddJsonParameter(module_data.intent.get(), param_name, param_value);
fuchsia::modular::ModuleParameterMapEntry parameter_entry;
auto link_path = MakeLinkPath(link_path_name);
fidl::Clone(module_data.module_path, &link_path.module_path);
parameter_entry.name = param_name;
fidl::Clone(link_path, &parameter_entry.link_path);
module_data.parameter_map.entries.push_back(std::move(parameter_entry));
SetLinkValue(story_storage_.get(), link_path, param_value);
WriteModuleData(story_storage_.get(), std::move(module_data));
}
std::unique_ptr<AddModCommandRunner> runner_;
std::unique_ptr<SessionStorage> session_storage_;
std::unique_ptr<StoryStorage> story_storage_;
fuchsia::modular::ModuleResolverPtr module_resolver_;
fuchsia::modular::EntityResolverPtr entity_resolver_;
std::string story_id_;
std::unique_ptr<ModuleResolverFake> fake_module_resolver_ =
std::make_unique<ModuleResolverFake>();
std::unique_ptr<EntityResolverFake> fake_entity_resolver_ =
std::make_unique<EntityResolverFake>();
};
TEST_F(AddModCommandRunnerTest, ExecuteIntentWithIntentHandler) {
// Set up command
auto intent = CreateEmptyIntent("intent_action", "mod_url");
intent.parameters.resize(0);
auto command = MakeAddModCommand("mod", "parent_mod", 0.5, intent);
auto manifest = fuchsia::modular::ModuleManifest::New();
manifest->intent_filters.push_back(MakeIntentFilter("intent_action", {}));
manifest->binary = "mod_url";
// Set up fake module resolver and set validation of GetModuleManifest call.
fuchsia::modular::ModuleManifestPtr manifest_out;
fuchsia::modular::ModuleManifestPtr manifest_to_add;
fidl::Clone(manifest, &manifest_out);
fidl::Clone(manifest, &manifest_to_add);
fake_module_resolver_->SetManifest(std::move(manifest_out));
fake_module_resolver_->SetGetModuleManifestValidation(
[&](const fidl::StringPtr& module_id) {
EXPECT_EQ(intent.handler, module_id);
});
fuchsia::modular::FindModulesResult res;
res.module_id = "mod_url";
res.manifest = std::move(manifest_to_add);
fake_module_resolver_->AddFindModulesResult(std::move(res));
// Run the command and assert results.
bool done = false;
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
ASSERT_EQ(fuchsia::modular::ExecuteStatus::OK,
result.status);
done = true;
});
RunLoopUntil([&] { return done; });
done = false;
std::vector<std::string> full_path{"parent_mod", "mod"};
story_storage_->ReadModuleData(std::move(full_path))
->Then([&](fuchsia::modular::ModuleDataPtr module_data) {
EXPECT_EQ("mod_url", module_data->module_url);
EXPECT_EQ(full_path, module_data->module_path);
EXPECT_FALSE(module_data->module_deleted);
EXPECT_EQ(fuchsia::modular::ModuleSource::EXTERNAL,
module_data->module_source);
EXPECT_EQ(0.5, module_data->surface_relation->emphasis);
EXPECT_TRUE(AreIntentsEqual(intent, *module_data->intent));
EXPECT_EQ(0u, module_data->parameter_map.entries.size());
done = true;
});
RunLoopUntil([&] { return done; });
}
// Explicitly leave surface_parent_mod_name as null when providing the Intent.
// We should tolerate this, and initialize it to a zero-length vector
// internally.
TEST_F(AddModCommandRunnerTest, ExecuteIntentWithIntentHandler_NoParent) {
auto intent = CreateEmptyIntent("intent_action", "mod_url");
intent.parameters.resize(0);
auto command =
MakeAddModCommand("mod", "" /* parent mod is null */, 0.5, intent);
auto manifest = fuchsia::modular::ModuleManifest::New();
manifest->intent_filters.push_back(MakeIntentFilter("intent_action", {}));
manifest->binary = "mod_url";
// Set up fake module resolver and set validation of GetModuleManifest call.
fuchsia::modular::ModuleManifestPtr manifest_out;
fuchsia::modular::ModuleManifestPtr manifest_to_add;
fidl::Clone(manifest, &manifest_out);
fidl::Clone(manifest, &manifest_to_add);
fake_module_resolver_->SetManifest(std::move(manifest_out));
fake_module_resolver_->SetGetModuleManifestValidation(
[&](const fidl::StringPtr& module_id) {
EXPECT_EQ(intent.handler, module_id);
});
fuchsia::modular::FindModulesResult res;
res.module_id = "mod_url";
res.manifest = std::move(manifest_to_add);
fake_module_resolver_->AddFindModulesResult(std::move(res));
// Run the command and assert results.
bool done = false;
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
ASSERT_EQ(fuchsia::modular::ExecuteStatus::OK,
result.status);
done = true;
});
RunLoopUntil([&] { return done; });
done = false;
std::vector<std::string> full_path{"mod"};
story_storage_->ReadModuleData(std::move(full_path))
->Then([&](fuchsia::modular::ModuleDataPtr module_data) {
EXPECT_EQ("mod_url", module_data->module_url);
EXPECT_EQ(full_path, module_data->module_path);
EXPECT_FALSE(module_data->module_deleted);
EXPECT_EQ(fuchsia::modular::ModuleSource::EXTERNAL,
module_data->module_source);
EXPECT_EQ(0.5, module_data->surface_relation->emphasis);
EXPECT_TRUE(AreIntentsEqual(intent, *module_data->intent));
EXPECT_EQ(0u, module_data->parameter_map.entries.size());
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(AddModCommandRunnerTest, ExecuteIntentThatNeedsResolution) {
auto reference =
fake_entity_resolver_->AddEntity({{"entity_type1", "entity_data"}});
InitParentMod("parent_mod", "param", R"({"@type": "baz"})",
"parent_link_name");
// Set up command
auto intent = CreateEmptyIntent("intent_action");
AddJsonParameter(&intent, "param_json", R"({"@type": "foo"})");
AddEntityRefParameter(&intent, "param_ref", reference);
AddEntityTypeParameter(&intent, "param_type", {"entity_type2"});
auto command = MakeAddModCommand("mod", "parent_mod", 0.5, intent);
auto manifest = fuchsia::modular::ModuleManifest::New();
manifest->binary = "mod_url";
manifest->intent_filters.push_back(MakeIntentFilter("intent_action", {}));
fuchsia::modular::FindModulesResult result;
result.module_id = "mod_url";
fidl::Clone(manifest, &result.manifest);
fake_module_resolver_->AddFindModulesResult(std::move(result));
// Since we are mocking module resolver and returning the expected |result|,
// ensure that at least it's being called with the right parameters.
fake_module_resolver_->SetFindModulesValidation(
[&](const fuchsia::modular::FindModulesQuery& query) {
EXPECT_EQ("intent_action", query.action);
auto& constraints = query.parameter_constraints;
EXPECT_EQ(3u, constraints.size());
for (auto& constraint : constraints) {
EXPECT_EQ(1u, constraint.param_types.size());
}
EXPECT_EQ("param_json", constraints.at(0).param_name);
EXPECT_EQ("foo", constraints.at(0).param_types.at(0));
EXPECT_EQ("param_ref", constraints.at(1).param_name);
EXPECT_EQ("entity_type1", constraints.at(1).param_types.at(0));
EXPECT_EQ("param_ref", constraints.at(1).param_name);
EXPECT_EQ("entity_type1", constraints.at(1).param_types.at(0));
EXPECT_EQ("param_type", constraints.at(2).param_name);
EXPECT_EQ("entity_type2", constraints.at(2).param_types.at(0));
});
bool done{};
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
EXPECT_EQ(fuchsia::modular::ExecuteStatus::OK,
result.status);
done = true;
});
RunLoopUntil([&] { return done; });
done = false;
std::vector<std::string> full_path{"parent_mod", "mod"};
fuchsia::modular::ModuleDataPtr module_data;
story_storage_->ReadModuleData(std::move(full_path))
->Then([&](fuchsia::modular::ModuleDataPtr result) {
module_data = std::move(result);
done = true;
});
RunLoopUntil([&] { return done; });
EXPECT_EQ("mod_url", module_data->module_url);
EXPECT_EQ(full_path, module_data->module_path);
EXPECT_FALSE(module_data->module_deleted);
EXPECT_EQ(fuchsia::modular::ModuleSource::EXTERNAL,
module_data->module_source);
EXPECT_EQ(0.5, module_data->surface_relation->emphasis);
EXPECT_TRUE(AreIntentsEqual(intent, *module_data->intent));
EXPECT_EQ(3u, module_data->parameter_map.entries.size());
// Verify parameters
auto& link_path1 = module_data->parameter_map.entries.at(0).link_path;
EXPECT_EQ("param_json", module_data->parameter_map.entries.at(0).name);
EXPECT_EQ(full_path, link_path1.module_path);
EXPECT_EQ("param_json", link_path1.link_name);
EXPECT_EQ(R"({"@type": "foo"})",
GetLinkValue(story_storage_.get(), link_path1));
auto& link_path2 = module_data->parameter_map.entries.at(1).link_path;
EXPECT_EQ("param_ref", module_data->parameter_map.entries.at(1).name);
EXPECT_EQ(full_path, link_path2.module_path);
EXPECT_EQ("param_ref", link_path2.link_name);
EXPECT_EQ(R"({"@entityRef":")" + *reference + R"("})",
GetLinkValue(story_storage_.get(), link_path2));
auto& link_path3 = module_data->parameter_map.entries.at(2).link_path;
EXPECT_EQ("param_type", module_data->parameter_map.entries.at(2).name);
EXPECT_EQ(full_path, link_path3.module_path);
EXPECT_EQ("param_type", link_path3.link_name);
EXPECT_EQ("null", GetLinkValue(story_storage_.get(), link_path3));
}
TEST_F(AddModCommandRunnerTest, ExecuteNoModulesFound) {
fuchsia::modular::Intent intent;
fuchsia::modular::AddMod add_mod;
intent.Clone(&add_mod.intent);
add_mod.mod_name.push_back("mymod");
add_mod.intent.action = "intent_action";
fuchsia::modular::StoryCommand command;
command.set_add_mod(std::move(add_mod));
bool done{};
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
EXPECT_EQ(
fuchsia::modular::ExecuteStatus::NO_MODULES_FOUND,
result.status);
EXPECT_EQ("Resolution of intent gave zero results.",
result.error_message);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(AddModCommandRunnerTest, ExecuteInvalidParameter) {
// Set up command
auto intent = CreateEmptyIntent("intent_action", "mod_url");
AddInvalidParameter(&intent, "invalid_param");
auto command = MakeAddModCommand("mod", "parent_mod", 0.5, intent);
auto manifest = fuchsia::modular::ModuleManifest::New();
manifest->binary = "mod_url";
manifest->intent_filters.push_back(MakeIntentFilter("intent_action", {}));
// Set up fake module resolver.
fuchsia::modular::ModuleManifestPtr manifest_out;
fidl::Clone(manifest, &manifest_out);
fake_module_resolver_->SetManifest(std::move(manifest_out));
// Run the command and assert results.
bool done{};
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
EXPECT_EQ(fuchsia::modular::ExecuteStatus::INVALID_COMMAND,
result.status);
EXPECT_EQ(
"Invalid data for parameter with name: "
"invalid_param",
result.error_message);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(AddModCommandRunnerTest, ExecuteInvalidParameterWithResulution) {
// Set up command
auto intent = CreateEmptyIntent("intent_action");
AddInvalidParameter(&intent, "invalid_param");
auto command = MakeAddModCommand("mod", "parent_mod", 0.5, intent);
// Set up fake module resolver.
auto manifest = fuchsia::modular::ModuleManifest::New();
manifest->binary = "mod_url";
manifest->intent_filters.push_back(MakeIntentFilter("intent_action", {}));
fuchsia::modular::FindModulesResult result;
result.module_id = "mod_url";
fidl::Clone(manifest, &result.manifest);
fake_module_resolver_->AddFindModulesResult(std::move(result));
// Run the command and assert results.
bool done{};
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
EXPECT_EQ(fuchsia::modular::ExecuteStatus::INVALID_COMMAND,
result.status);
EXPECT_EQ(
"Invalid data for parameter with name: "
"invalid_param",
result.error_message);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(AddModCommandRunnerTest, ExecuteNullHandlerAndParameter) {
// Set up command
auto intent = CreateEmptyIntent("intent_action");
AddParameterWithoutName(&intent);
auto command = MakeAddModCommand("mod", "parent_mod", 0.5, intent);
// Run the command and assert results.
bool done{};
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
EXPECT_EQ(fuchsia::modular::ExecuteStatus::INVALID_COMMAND,
result.status);
EXPECT_EQ(
"A null-named module parameter is not allowed "
"when using fuchsia::modular::Intent.",
result.error_message);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(AddModCommandRunnerTest, ExecuteInvalidJsonParamResolution) {
// Set up command
auto intent = CreateEmptyIntent("intent_action");
AddJsonParameter(&intent, "invalid_param", "x}");
auto command = MakeAddModCommand("mod", "parent_mod", 0.5, intent);
// Set up fake module resolver.
auto manifest = fuchsia::modular::ModuleManifest::New();
manifest->binary = "mod_url";
manifest->intent_filters.push_back(MakeIntentFilter("intent_action", {}));
fuchsia::modular::FindModulesResult result;
result.module_id = "mod_url";
fidl::Clone(manifest, &result.manifest);
fake_module_resolver_->AddFindModulesResult(std::move(result));
// Run the command and assert results.
bool done{};
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
EXPECT_EQ(fuchsia::modular::ExecuteStatus::INVALID_COMMAND,
result.status);
EXPECT_EQ("Mal-formed JSON in parameter: invalid_param",
result.error_message);
done = true;
});
RunLoopUntil([&] { return done; });
}
TEST_F(AddModCommandRunnerTest, UpdatesModIfItExists) {
// Set up command
auto intent = CreateEmptyIntent("intent_action", "mod_url");
AddJsonParameter(&intent, "param_json", R"({"@type": "foo"})");
auto command = MakeAddModCommand("mod", "parent_mod", 0.5, intent);
auto manifest = fuchsia::modular::ModuleManifest::New();
manifest->binary = "mod_url";
manifest->intent_filters.push_back(MakeIntentFilter(
"intent_action",
{fuchsia::modular::ParameterConstraint{"param_json", "foo"}}));
// Set up fake module resolver, set validaiton of GetModuleManifest call, and
// a dummy result.
{
fuchsia::modular::ModuleManifestPtr manifest_out;
fidl::Clone(manifest, &manifest_out);
fake_module_resolver_->SetManifest(std::move(manifest_out));
fuchsia::modular::FindModulesResult res;
res.module_id = "mod_url";
res.manifest = std::move(manifest);
fake_module_resolver_->AddFindModulesResult(std::move(res));
}
// Add a mod to begin with.
bool done{};
runner_->Execute(story_id_, story_storage_.get(), std::move(command),
[&](fuchsia::modular::ExecuteResult result) {
ASSERT_EQ(fuchsia::modular::ExecuteStatus::OK,
result.status);
done = true;
});
RunLoopUntil([&] { return done; });
// Get the link path for the param of the mod we created.
fuchsia::modular::LinkPath link_path;
std::vector<std::string> full_path{"parent_mod", "mod"};
story_storage_->ReadModuleData(full_path)->Then(
[&](fuchsia::modular::ModuleDataPtr result) {
for (auto& entry : result->parameter_map.entries) {
if (entry.name == "param_json") {
fidl::Clone(entry.link_path, &link_path);
}
}
});
// Create AddMod intent for a mod with the same name as the one we created
// previously.
auto intent2 = CreateEmptyIntent("intent_action", "mod_url");
AddJsonParameter(&intent2, "param_json", R"({"@type": "baz"})");
fuchsia::modular::AddMod add_mod;
add_mod.mod_name = {"parent_mod", "mod"};
intent2.Clone(&add_mod.intent);
fuchsia::modular::StoryCommand command2;
command2.set_add_mod(std::move(add_mod));
{
fuchsia::modular::ModuleManifestPtr manifest_out;
fidl::Clone(manifest, &manifest_out);
fake_module_resolver_->SetManifest(std::move(manifest_out));
fuchsia::modular::FindModulesResult res;
res.module_id = "mod_url";
res.manifest = std::move(manifest);
fake_module_resolver_->AddFindModulesResult(std::move(res));
}
done = false;
runner_->Execute(story_id_, story_storage_.get(), std::move(command2),
[&](fuchsia::modular::ExecuteResult result) {
ASSERT_EQ(fuchsia::modular::ExecuteStatus::OK,
result.status);
done = true;
});
RunLoopUntil([&] { return done; });
done = false;
// Verify we updated an existing link.
story_storage_->GetLinkValue(link_path)->Then(
[&](StoryStorage::Status status, fidl::StringPtr v) {
EXPECT_EQ(R"({"@type": "baz"})", *v);
done = true;
});
RunLoopUntil([&] { return done; });
}
} // namespace
} // namespace modular