blob: f74cf6e5787339820f41bff47d079f44c87291c8 [file] [log] [blame]
// Copyright 2018 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 <fuchsia/modular/cpp/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/zx/channel.h>
#include "base/fuchsia/file_utils.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind_test_util.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "fuchsia/base/agent_impl.h"
#include "fuchsia/base/fake_component_context.h"
#include "fuchsia/base/fit_adapter.h"
#include "fuchsia/base/frame_test_util.h"
#include "fuchsia/base/mem_buffer_util.h"
#include "fuchsia/base/result_receiver.h"
#include "fuchsia/base/string_util.h"
#include "fuchsia/base/test_devtools_list_fetcher.h"
#include "fuchsia/base/test_navigation_listener.h"
#include "fuchsia/base/url_request_rewrite_test_util.h"
#include "fuchsia/runners/cast/cast_runner.h"
#include "fuchsia/runners/cast/fake_application_config_manager.h"
#include "fuchsia/runners/cast/test_api_bindings.h"
#include "fuchsia/runners/common/web_component.h"
#include "fuchsia/runners/common/web_content_runner.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/http_request.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace castrunner {
namespace {
const char kTestServerRoot[] =
FILE_PATH_LITERAL("fuchsia/runners/cast/testdata");
void ComponentErrorHandler(zx_status_t status) {
ZX_LOG(ERROR, status) << "Component launch failed";
ADD_FAILURE();
}
class FakeUrlRequestRewriteRulesProvider
: public chromium::cast::UrlRequestRewriteRulesProvider {
public:
explicit FakeUrlRequestRewriteRulesProvider(sys::OutgoingDirectory* directory)
: binding_(directory, this) {}
~FakeUrlRequestRewriteRulesProvider() override = default;
private:
void GetUrlRequestRewriteRules(
GetUrlRequestRewriteRulesCallback callback) override {
// Only send the rules once. They do not expire
if (rules_sent_)
return;
rules_sent_ = true;
std::vector<fuchsia::web::UrlRequestRewrite> rewrites;
rewrites.push_back(cr_fuchsia::CreateRewriteAddHeaders("Test", "Value"));
fuchsia::web::UrlRequestRewriteRule rule;
rule.set_rewrites(std::move(rewrites));
std::vector<fuchsia::web::UrlRequestRewriteRule> rules;
rules.push_back(std::move(rule));
callback(std::move(rules));
}
bool rules_sent_ = false;
const base::fuchsia::ScopedServiceBinding<
chromium::cast::UrlRequestRewriteRulesProvider>
binding_;
DISALLOW_COPY_AND_ASSIGN(FakeUrlRequestRewriteRulesProvider);
};
class FakeApplicationControllerReceiver
: public chromium::cast::ApplicationControllerReceiver {
public:
FakeApplicationControllerReceiver() = default;
~FakeApplicationControllerReceiver() final = default;
chromium::cast::ApplicationController* controller() {
if (!controller_)
return nullptr;
return controller_.get();
}
private:
// chromium::cast::ApplicationControllerReceiver implementation.
void SetApplicationController(
fidl::InterfaceHandle<chromium::cast::ApplicationController> controller)
final {
controller_ = controller.Bind();
}
chromium::cast::ApplicationControllerPtr controller_;
DISALLOW_COPY_AND_ASSIGN(FakeApplicationControllerReceiver);
};
class FakeComponentState : public cr_fuchsia::AgentImpl::ComponentStateBase {
public:
FakeComponentState(
base::StringPiece component_url,
chromium::cast::ApplicationConfigManager* app_config_manager,
chromium::cast::ApiBindings* bindings_manager,
bool provide_controller_receiver)
: ComponentStateBase(component_url),
app_config_binding_(outgoing_directory(), app_config_manager),
url_request_rules_provider_(
std::make_unique<FakeUrlRequestRewriteRulesProvider>(
outgoing_directory())) {
if (bindings_manager) {
bindings_manager_binding_ = std::make_unique<
base::fuchsia::ScopedServiceBinding<chromium::cast::ApiBindings>>(
outgoing_directory(), bindings_manager);
}
if (provide_controller_receiver) {
controller_receiver_binding_.emplace(outgoing_directory(),
&controller_receiver_);
}
}
~FakeComponentState() override {
if (on_delete_)
std::move(on_delete_).Run();
}
FakeApplicationControllerReceiver* controller_receiver() {
return &controller_receiver_;
}
void set_on_delete(base::OnceClosure on_delete) {
on_delete_ = std::move(on_delete);
}
protected:
const base::fuchsia::ScopedServiceBinding<
chromium::cast::ApplicationConfigManager>
app_config_binding_;
std::unique_ptr<
base::fuchsia::ScopedServiceBinding<chromium::cast::ApiBindings>>
bindings_manager_binding_;
std::unique_ptr<FakeUrlRequestRewriteRulesProvider>
url_request_rules_provider_;
FakeApplicationControllerReceiver controller_receiver_;
base::Optional<base::fuchsia::ScopedServiceBinding<
chromium::cast::ApplicationControllerReceiver>>
controller_receiver_binding_;
base::OnceClosure on_delete_;
DISALLOW_COPY_AND_ASSIGN(FakeComponentState);
};
} // namespace
class CastRunnerIntegrationTest : public testing::Test {
public:
CastRunnerIntegrationTest()
: run_timeout_(
TestTimeouts::action_timeout(),
base::MakeExpectedNotRunClosure(FROM_HERE, "Run() timed out.")) {
// Create the CastRunner, published into |outgoing_directory_|.
constexpr fuchsia::web::ContextFeatureFlags kFeatures = {};
fuchsia::web::CreateContextParams create_context_params =
WebContentRunner::BuildCreateContextParams(
fidl::InterfaceHandle<fuchsia::io::Directory>(), kFeatures);
const uint16_t kRemoteDebuggingAnyPort = 0;
create_context_params.set_remote_debugging_port(kRemoteDebuggingAnyPort);
cast_runner_ = std::make_unique<CastRunner>(
&outgoing_directory_,
WebContentRunner::CreateWebContext(std::move(create_context_params)));
// Connect to the CastRunner's fuchsia.sys.Runner interface.
fidl::InterfaceHandle<fuchsia::io::Directory> directory;
outgoing_directory_.GetOrCreateDirectory("svc")->Serve(
fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_WRITABLE,
directory.NewRequest().TakeChannel());
sys::ServiceDirectory public_directory_client(std::move(directory));
cast_runner_ptr_ = public_directory_client.Connect<fuchsia::sys::Runner>();
cast_runner_ptr_.set_error_handler([](zx_status_t status) {
ZX_LOG(ERROR, status) << "CastRunner closed channel.";
ADD_FAILURE();
});
}
void SetUp() override {
test_server_.ServeFilesFromSourceDirectory(kTestServerRoot);
net::test_server::RegisterDefaultHandlers(&test_server_);
ASSERT_TRUE(test_server_.Start());
}
void TearDown() override {
// Disconnect the CastRunner & let things tear-down.
cast_runner_ptr_.Unbind();
base::RunLoop().RunUntilIdle();
}
fuchsia::sys::ComponentControllerPtr StartCastComponent(
base::StringPiece component_url) {
DCHECK(!component_state_);
// Create a FakeComponentContext and publish it into component_services_.
component_context_ = std::make_unique<cr_fuchsia::FakeComponentContext>(
base::BindRepeating(&CastRunnerIntegrationTest::OnComponentConnect,
base::Unretained(this)),
&component_services_, component_url);
// Configure the Runner, including a service directory channel to publish
// services to.
fidl::InterfaceHandle<fuchsia::io::Directory> directory;
component_services_.GetOrCreateDirectory("svc")->Serve(
fuchsia::io::OPEN_RIGHT_READABLE | fuchsia::io::OPEN_RIGHT_WRITABLE,
directory.NewRequest().TakeChannel());
fuchsia::sys::StartupInfo startup_info;
startup_info.launch_info.url = component_url.as_string();
fidl::InterfaceHandle<fuchsia::io::Directory> outgoing_directory;
startup_info.launch_info.directory_request =
outgoing_directory.NewRequest().TakeChannel();
fidl::InterfaceHandle<::fuchsia::io::Directory> svc_directory;
CHECK_EQ(fdio_service_connect_at(
outgoing_directory.channel().get(), "svc",
svc_directory.NewRequest().TakeChannel().release()),
ZX_OK);
component_services_client_ =
std::make_unique<base::fuchsia::ServiceDirectoryClient>(
std::move(svc_directory));
// Place the ServiceDirectory in the |flat_namespace|.
startup_info.flat_namespace.paths.emplace_back(
base::fuchsia::kServiceDirectoryPath);
startup_info.flat_namespace.directories.emplace_back(
directory.TakeChannel());
fuchsia::sys::Package package;
package.resolved_url = component_url.as_string();
fuchsia::sys::ComponentControllerPtr component_controller;
cast_runner_ptr_->StartComponent(std::move(package),
std::move(startup_info),
component_controller.NewRequest());
EXPECT_TRUE(component_controller.is_bound());
return component_controller;
}
protected:
std::unique_ptr<cr_fuchsia::AgentImpl::ComponentStateBase> OnComponentConnect(
base::StringPiece component_url) {
auto component_state = std::make_unique<FakeComponentState>(
component_url, &app_config_manager_,
(provide_api_bindings_ ? &api_bindings_ : nullptr),
provide_controller_receiver_);
component_state_ = component_state.get();
return component_state;
}
const base::RunLoop::ScopedRunTimeoutForTest run_timeout_;
base::test::SingleThreadTaskEnvironment task_environment_{
base::test::SingleThreadTaskEnvironment::MainThreadType::IO};
net::EmbeddedTestServer test_server_;
// Returns fake Cast application information to the CastRunner.
FakeApplicationConfigManager app_config_manager_;
TestApiBindings api_bindings_;
// If set, publishes a ApplicationControllerReceiver service from the agent.
// TODO(crbug.com/953958): Remove this flag and make provisioning of the
// receiver service mandatory once CastAgent supports it.
bool provide_controller_receiver_ = true;
// If set, publishes an ApiBindings service from the Agent.
bool provide_api_bindings_ = true;
// Incoming service directory, ComponentContext and per-component state.
sys::OutgoingDirectory component_services_;
std::unique_ptr<cr_fuchsia::FakeComponentContext> component_context_;
std::unique_ptr<base::fuchsia::ServiceDirectoryClient>
component_services_client_;
FakeComponentState* component_state_ = nullptr;
// ServiceDirectory into which the CastRunner will publish itself.
sys::OutgoingDirectory outgoing_directory_;
std::unique_ptr<CastRunner> cast_runner_;
fuchsia::sys::RunnerPtr cast_runner_ptr_;
DISALLOW_COPY_AND_ASSIGN(CastRunnerIntegrationTest);
};
// A basic integration test ensuring a basic cast request launches the right
// URL in the Chromium service.
TEST_F(CastRunnerIntegrationTest, BasicRequest) {
const char kBlankAppId[] = "00000000";
const char kBlankAppPath[] = "/defaultresponse";
app_config_manager_.AddAppMapping(kBlankAppId,
test_server_.GetURL(kBlankAppPath), false);
// Launch the test-app component.
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent(base::StringPrintf("cast:%s", kBlankAppId));
component_controller.set_error_handler(&ComponentErrorHandler);
// Access the NavigationController from the WebComponent. The test will hang
// here if no WebComponent was created.
fuchsia::web::NavigationControllerPtr nav_controller;
{
base::RunLoop run_loop;
cr_fuchsia::ResultReceiver<WebComponent*> web_component(
run_loop.QuitClosure());
cast_runner_->GetWebComponentForTest(web_component.GetReceiveCallback());
run_loop.Run();
ASSERT_NE(*web_component, nullptr);
(*web_component)
->frame()
->GetNavigationController(nav_controller.NewRequest());
}
// Ensure the NavigationState has the expected URL.
{
base::RunLoop run_loop;
cr_fuchsia::ResultReceiver<fuchsia::web::NavigationState> nav_entry(
run_loop.QuitClosure());
nav_controller->GetVisibleEntry(
cr_fuchsia::CallbackToFitFunction(nav_entry.GetReceiveCallback()));
run_loop.Run();
ASSERT_TRUE(nav_entry->has_url());
EXPECT_EQ(nav_entry->url(), test_server_.GetURL(kBlankAppPath).spec());
}
// Verify that the component is torn down when |component_controller| is
// unbound.
base::RunLoop run_loop;
component_state_->set_on_delete(run_loop.QuitClosure());
component_controller.Unbind();
run_loop.Run();
}
TEST_F(CastRunnerIntegrationTest, ApiBindings) {
provide_api_bindings_ = true;
const char kBlankAppId[] = "00000000";
const char kBlankAppPath[] = "/echo.html";
app_config_manager_.AddAppMapping(kBlankAppId,
test_server_.GetURL(kBlankAppPath), false);
std::vector<chromium::cast::ApiBinding> binding_list;
chromium::cast::ApiBinding echo_binding;
echo_binding.set_before_load_script(cr_fuchsia::MemBufferFromString(
"window.echo = cast.__platform__.PortConnector.bind('echoService');",
"test"));
binding_list.emplace_back(std::move(echo_binding));
api_bindings_.set_bindings(std::move(binding_list));
// Launch the test-app component.
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent(base::StringPrintf("cast:%s", kBlankAppId));
component_controller.set_error_handler(&ComponentErrorHandler);
fuchsia::web::MessagePortPtr port =
api_bindings_.RunUntilMessagePortReceived("echoService").Bind();
fuchsia::web::WebMessage message;
message.set_data(cr_fuchsia::MemBufferFromString("ping", "ping-msg"));
port->PostMessage(std::move(message),
[](fuchsia::web::MessagePort_PostMessage_Result result) {
EXPECT_TRUE(result.is_response());
});
base::RunLoop response_loop;
cr_fuchsia::ResultReceiver<fuchsia::web::WebMessage> response(
response_loop.QuitClosure());
port->ReceiveMessage(
cr_fuchsia::CallbackToFitFunction(response.GetReceiveCallback()));
response_loop.Run();
std::string response_string;
EXPECT_TRUE(
cr_fuchsia::StringFromMemBuffer(response->data(), &response_string));
EXPECT_EQ("ack ping", response_string);
}
TEST_F(CastRunnerIntegrationTest, IncorrectCastAppId) {
// Launch the a component with an invalid Cast app Id.
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent("cast:99999999");
component_controller.set_error_handler(&ComponentErrorHandler);
// Run the loop until the ComponentController is dropped, or a WebComponent is
// created.
base::RunLoop run_loop;
component_controller.set_error_handler([&run_loop](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
run_loop.Quit();
});
cr_fuchsia::ResultReceiver<WebComponent*> web_component(
run_loop.QuitClosure());
cast_runner_->GetWebComponentForTest(web_component.GetReceiveCallback());
run_loop.Run();
EXPECT_FALSE(web_component.has_value());
}
TEST_F(CastRunnerIntegrationTest, UrlRequestRewriteRulesProvider) {
const char kEchoAppId[] = "00000000";
const char kEchoAppPath[] = "/echoheader?Test";
const GURL echo_app_url = test_server_.GetURL(kEchoAppPath);
app_config_manager_.AddAppMapping(kEchoAppId, echo_app_url, false);
// Launch the test-app component.
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent(base::StringPrintf("cast:%s", kEchoAppId));
component_controller.set_error_handler(&ComponentErrorHandler);
WebComponent* web_component = nullptr;
{
base::RunLoop run_loop;
cr_fuchsia::ResultReceiver<WebComponent*> web_component_receiver(
run_loop.QuitClosure());
cast_runner_->GetWebComponentForTest(
web_component_receiver.GetReceiveCallback());
run_loop.Run();
ASSERT_NE(*web_component_receiver, nullptr);
web_component = *web_component_receiver;
}
// Bind a TestNavigationListener to the Frame.
cr_fuchsia::TestNavigationListener navigation_listener;
fidl::Binding<fuchsia::web::NavigationEventListener>
navigation_listener_binding(&navigation_listener);
web_component->frame()->SetNavigationEventListener(
navigation_listener_binding.NewBinding());
navigation_listener.RunUntilUrlEquals(echo_app_url);
// Check the header was properly set.
base::Optional<base::Value> result = cr_fuchsia::ExecuteJavaScript(
web_component->frame(), "document.body.innerText");
ASSERT_TRUE(result);
ASSERT_TRUE(result->is_string());
EXPECT_EQ(result->GetString(), "Value");
}
TEST_F(CastRunnerIntegrationTest, ApplicationControllerBound) {
const char kCastChannelAppId[] = "00000001";
const char kCastChannelAppPath[] = "/defaultresponse";
app_config_manager_.AddAppMapping(
kCastChannelAppId, test_server_.GetURL(kCastChannelAppPath), false);
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent(base::StringPrintf("cast:%s", kCastChannelAppId));
// Spin the message loop to handle creation of the component state.
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(component_state_);
EXPECT_TRUE(component_state_->controller_receiver()->controller());
}
// Verify that we gracefully handle the interim case where the Agent doesn't
// publish the ApplicationController service.
// TODO(crbug.com/953958): Remove this test case once CastAgent provides the
// service.
TEST_F(CastRunnerIntegrationTest, ApplicationControllerNotSupported) {
const char kCastChannelAppId[] = "00000001";
const char kCastChannelAppPath[] = "/defaultresponse";
provide_controller_receiver_ = false;
app_config_manager_.AddAppMapping(
kCastChannelAppId, test_server_.GetURL(kCastChannelAppPath), false);
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent(base::StringPrintf("cast:%s", kCastChannelAppId));
// Spin the message loop to handle creation of the component state.
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(component_state_);
EXPECT_FALSE(component_state_->controller_receiver()->controller());
}
// Verify that we can connect to the Lifecycle interface and terminate the
// component. Service connection is sent immediately after the component start
// request, so this test also verifies that the component doesn't start handling
// incoming service requests before the service has been published.
TEST_F(CastRunnerIntegrationTest, Lifecycle) {
const char kCastChannelAppId[] = "00000001";
const char kCastChannelAppPath[] = "/defaultresponse";
provide_controller_receiver_ = false;
app_config_manager_.AddAppMapping(
kCastChannelAppId, test_server_.GetURL(kCastChannelAppPath), false);
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent(base::StringPrintf("cast:%s", kCastChannelAppId));
fuchsia::modular::LifecyclePtr lifecycle;
component_services_client_->ConnectToService(lifecycle.NewRequest());
lifecycle->Terminate();
base::RunLoop run_loop;
component_controller.set_error_handler([&run_loop](zx_status_t status) {
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED);
run_loop.Quit();
});
run_loop.Run();
}
// Verify an App launched with remote debugging enabled is properly reachable.
TEST_F(CastRunnerIntegrationTest, RemoteDebugging) {
const char kBlankAppId[] = "00000000";
const char kBlankAppPath[] = "/defaultresponse";
const GURL kBlankAppUrl = test_server_.GetURL(kBlankAppPath);
app_config_manager_.AddAppMapping(kBlankAppId, kBlankAppUrl, true);
// Launch the test-app component.
fuchsia::sys::ComponentControllerPtr component_controller =
StartCastComponent(base::StringPrintf("cast:%s", kBlankAppId));
component_controller.set_error_handler(&ComponentErrorHandler);
// Get the remote debugging port from the Context.
uint16_t remote_debugging_port = 0;
{
base::RunLoop run_loop;
cr_fuchsia::ResultReceiver<
fuchsia::web::Context_GetRemoteDebuggingPort_Result>
port_receiver(run_loop.QuitClosure());
cast_runner_->context()->GetRemoteDebuggingPort(
cr_fuchsia::CallbackToFitFunction(port_receiver.GetReceiveCallback()));
run_loop.Run();
ASSERT_TRUE(port_receiver->is_response());
remote_debugging_port = port_receiver->response().port;
ASSERT_TRUE(remote_debugging_port != 0);
}
// Connect to the debug service and ensure we get the proper response.
base::Value devtools_list =
cr_fuchsia::GetDevToolsListFromPort(remote_debugging_port);
ASSERT_TRUE(devtools_list.is_list());
EXPECT_EQ(devtools_list.GetList().size(), 1u);
base::Value* devtools_url = devtools_list.GetList()[0].FindPath("url");
ASSERT_TRUE(devtools_url->is_string());
EXPECT_EQ(devtools_url->GetString(), kBlankAppUrl.spec());
}
} // namespace castrunner