chromium/fuchsia_web/runners/cast/cast_runner_integration_test.cc

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <chromium/cast/cpp/fidl.h>
#include <fuchsia/camera3/cpp/fidl.h>
#include <fuchsia/legacymetrics/cpp/fidl.h>
#include <fuchsia/media/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <fuchsia/web/cpp/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/zx/eventpair.h>

#include <string_view>
#include <utility>
#include <vector>

#include "base/auto_reset.h"
#include "base/base_paths.h"
#include "base/files/file_util.h"
#include "base/fuchsia/file_utils.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/mem_buffer_util.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "build/chromecast_buildflags.h"
#include "components/fuchsia_component_support/dynamic_component_host.h"
#include "fuchsia_web/common/string_util.h"
#include "fuchsia_web/common/test/fit_adapter.h"
#include "fuchsia_web/common/test/frame_for_test.h"
#include "fuchsia_web/common/test/frame_test_util.h"
#include "fuchsia_web/common/test/test_debug_listener.h"
#include "fuchsia_web/common/test/test_devtools_list_fetcher.h"
#include "fuchsia_web/common/test/test_navigation_listener.h"
#include "fuchsia_web/common/test/url_request_rewrite_test_util.h"
#include "fuchsia_web/runners/cast/cast_runner.h"
#include "fuchsia_web/runners/cast/cast_runner_switches.h"
#include "fuchsia_web/runners/cast/test/cast_runner_features.h"
#include "fuchsia_web/runners/cast/test/cast_runner_launcher.h"
#include "fuchsia_web/runners/cast/test/fake_api_bindings.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

constexpr char kTestAppId[] = "00000000";
constexpr char kSecondTestAppId[] = "FFFFFFFF";

constexpr char kBlankAppUrl[] = "/defaultresponse";
constexpr char kEchoHeaderPath[] = "/echoheader?Test";

chromium::cast::ApplicationConfig CreateAppConfigWithTestData(
    std::string_view app_id,
    GURL url) {
  fuchsia::web::ContentDirectoryProvider provider;
  provider.set_name("testdata");

  base::FilePath pkg_path;
  CHECK(base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &pkg_path));

  provider.set_directory(base::OpenDirectoryHandle(
      pkg_path.AppendASCII("fuchsia_web/runners/cast/testdata")));
  std::vector<fuchsia::web::ContentDirectoryProvider> providers;
  providers.emplace_back(std::move(provider));

  auto app_config = FakeApplicationConfigManager::CreateConfig(app_id, url);
  app_config.set_content_directories_for_isolated_application(
      std::move(providers));
  return app_config;
}

class FakeUrlRequestRewriteRulesProvider final
    : public chromium::cast::UrlRequestRewriteRulesProvider {
 public:
  FakeUrlRequestRewriteRulesProvider() = default;
  ~FakeUrlRequestRewriteRulesProvider() override = default;

  FakeUrlRequestRewriteRulesProvider(
      const FakeUrlRequestRewriteRulesProvider&) = delete;
  FakeUrlRequestRewriteRulesProvider& operator=(
      const FakeUrlRequestRewriteRulesProvider&) = delete;

 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(CreateRewriteAddHeaders("Test", "TestHeaderValue"));
    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;
};

class FakeApplicationContext final : public chromium::cast::ApplicationContext {
 public:
  FakeApplicationContext() = default;
  ~FakeApplicationContext() override = default;

  FakeApplicationContext(const FakeApplicationContext&) = delete;
  FakeApplicationContext& operator=(const FakeApplicationContext&) = delete;

  chromium::cast::ApplicationController* application_controller() {
    if (!application_controller_) {
      return nullptr;
    }

    return application_controller_.get();
  }

  void WaitForSetApplicationController() {
    if (application_controller_) {
      return;
    }
    base::RunLoop loop;
    on_set_application_controller_ = loop.QuitClosure();
    loop.Run();
  }

  std::optional<int64_t> WaitForApplicationTerminated() {
    if (application_exit_code_.has_value()) {
      return application_exit_code_;
    }
    base::RunLoop loop;
    on_application_terminated_ = loop.QuitClosure();
    loop.Run();
    return application_exit_code_;
  }

 private:
  // chromium::cast::ApplicationContext implementation.
  void GetMediaSessionId(GetMediaSessionIdCallback callback) override {
    callback(1);
  }
  void SetApplicationController(
      fidl::InterfaceHandle<chromium::cast::ApplicationController>
          application_controller) override {
    application_controller_ = application_controller.Bind();
    if (on_set_application_controller_) {
      std::move(on_set_application_controller_).Run();
    }
  }
  void OnApplicationExit(int64_t exit_code) override {
    application_exit_code_ = exit_code;
    if (on_application_terminated_) {
      std::move(on_application_terminated_).Run();
    }
  }

  chromium::cast::ApplicationControllerPtr application_controller_;
  base::OnceClosure on_set_application_controller_;

  std::optional<int64_t> application_exit_code_;
  base::OnceClosure on_application_terminated_;
};

class TestCastComponent {
 public:
  // `test_realm_services` is used to connect to the test `Realm` exposed by
  // the CastRunnerLauncher, that contains a collection capable of resolving
  // and running Cast components.
  explicit TestCastComponent(const sys::ServiceDirectory& test_realm_services)
      : test_realm_services_(test_realm_services) {
    // Instantiate the per-instance service directory and fakes by default,
    // for tests to configure & add expectations to.
    services_.emplace();
  }

  ~TestCastComponent() { ShutdownComponent(); }

  TestCastComponent(const TestCastComponent&) = delete;
  TestCastComponent& operator=(const TestCastComponent&) = delete;

  void disable_offer_services() { offer_services_ = false; }
  void offer_closed_services() { offer_closed_services_ = true; }

  // Attempts to start the Cast activity identified by `app_id`, and to inject
  // script bindings to support querying JavaScript/DOM state via the
  // the `ExecuteJavaScript()` API (see below).
  // Note that this function will not return until the activity has actually
  // launched.
  void StartCastComponentWithQueryApi(std::string_view app_id = kTestAppId) {
    auto component_url = base::StrCat({"cast:", app_id});
    InjectQueryApi();
    StartCastComponent(component_url);
    WaitQueryApiConnected();
  }

  // Attempts to start the Cast activity identified by `app_id`.
  // Note that activity launch is asynchronous, tests that need to wait until
  // the activity has actually started (e.g. to interact with its
  // `ApplicationController`, etc), should normally use the
  // `StartCastComponentWithQueryApi()` call instead.
  void StartCastComponent(std::string_view component_url) {
    ASSERT_FALSE(component_) << "Component may only be started once";

    fidl::InterfaceHandle<fuchsia::io::Directory> services;
    if (offer_services_) {
      if (offer_closed_services_) {
        // Create the `services` channel, and immediately close the "request"
        // end, to simulate a missing service directory.
        std::ignore = services.NewRequest();
      } else {
        // Create a `fuchsia.io.Directory` connected to the directory of fake
        // services.
        zx_status_t status = services_->services.Serve(
            fuchsia::io::OpenFlags::RIGHT_READABLE |
                fuchsia::io::OpenFlags::RIGHT_WRITABLE |
                fuchsia::io::OpenFlags::DIRECTORY,
            services.NewRequest().TakeChannel());
        ZX_CHECK(status == ZX_OK, status) << "Serve()";
      }
    }

    // Create the new dynamic component in the test component collection for
    // the `cast_runner`.  The component is created with an Id chosen
    // at random to uniquely identify it, and supplied with `services` as
    // configured above.
    component_.emplace(
        test_realm_services_.Connect<fuchsia::component::Realm>(),
        test::CastRunnerLauncher::kTestCollectionName,
        base::Uuid::GenerateRandomV4().AsLowercaseString(), component_url,
        base::BindOnce(&TestCastComponent::OnComponentTeardown,
                       base::Unretained(this)),
        std::move(services));
  }

  // Executes |code| in the context of the test application and then returns
  // the result serialized as string. If the code evaluates to a promise then
  // execution is blocked until the promise is complete and the result of the
  // promise is returned.
  std::string ExecuteJavaScript(const std::string& code) {
    CHECK(test_port_);

    fuchsia::web::WebMessage message;
    message.set_data(base::MemBufferFromString(code, "test-msg"));
    test_port_->PostMessage(
        std::move(message),
        [](fuchsia::web::MessagePort_PostMessage_Result result) {
          EXPECT_TRUE(result.is_response());
        });

    base::test::TestFuture<fuchsia::web::WebMessage> response;
    test_port_->ReceiveMessage(CallbackToFitFunction(response.GetCallback()));
    EXPECT_TRUE(response.Wait());

    std::optional<std::string> response_string =
        base::StringFromMemBuffer(response.Get().data());
    EXPECT_TRUE(response_string.has_value());

    return response_string.value_or(std::string());
  }

  std::string QueryAppUrl() {
    return ExecuteJavaScript("window.location.href");
  }

  // Closes active connections to all services offered to the component,
  // to simulate the controlling agent tearing-down unexpectedly.
  void DisconnectServices() { services_.reset(); }

  // Destroys the component and runs until it is observed to have torn-down.
  void ShutdownComponent() {
    if (component_) {
      component_->Destroy();
      WaitForComponentDestroyed();
    }
  }

  // Runs until `component_` is observed to have torn-down.
  // Note that this may return before connections to services used by
  // the component have been observed to have closed.
  // This should be used after triggering component teardown, e.g. via an
  // explicit ComponentController.Kill() call, to wait for it to take effect.
  void WaitForComponentDestroyed() {
    ASSERT_TRUE(component_);
    base::RunLoop state_loop;
    base::AutoReset reset_callback(&on_component_destroyed_,
                                   state_loop.QuitClosure());
    state_loop.Run();
  }

  // Disables treatment of `component_` teardown as a test failure. This is
  // useful for test of teardown-time API behaviours.
  void SetIgnoreComponentDestroyed() {
    on_component_destroyed_ = base::DoNothing();
  }

  FakeApiBindingsImpl& api_bindings() { return services_->api_bindings; }
  FakeApplicationContext& application_context() {
    return services_->application_context;
  }

  sys::ServiceDirectory& exposed_by_component() {
    return component_->exposed();
  }

 private:
  // Used to manage fake services offered to each Cast component individually.
  // Most of the FIDL services used by Cast activities are ambient, provided by
  // the CastRunner itself. The services provided here are only those offered
  // directly to Cast activities by their owning agent.
  struct FakeComponentServices {
    FakeComponentServices()
        : api_bindings_binding(&services, &api_bindings),
          url_request_rewrite_rules_provider_binding(
              &services,
              &url_request_rewrite_rules_provider),
          context_binding(&services, &application_context) {}

    // Directory of services to offer to the Cast component.
    vfs::PseudoDir services;

    FakeApiBindingsImpl api_bindings;
    base::ScopedServiceBinding<chromium::cast::ApiBindings>
        api_bindings_binding;

    FakeUrlRequestRewriteRulesProvider url_request_rewrite_rules_provider;
    base::ScopedServiceBinding<chromium::cast::UrlRequestRewriteRulesProvider>
        url_request_rewrite_rules_provider_binding;

    FakeApplicationContext application_context;
    base::ScopedServiceBinding<chromium::cast::ApplicationContext>
        context_binding;
  };

  void InjectQueryApi() {
    // Inject an API which can be used to evaluate arbitrary Javascript and
    // return the results over a MessagePort.
    std::vector<chromium::cast::ApiBinding> binding_list;
    chromium::cast::ApiBinding eval_js_binding;
    eval_js_binding.set_before_load_script(base::MemBufferFromString(
        "function valueOrUndefinedString(value) {"
        "    return (typeof(value) == 'undefined') ? 'undefined' : value;"
        "}"
        "window.addEventListener('DOMContentLoaded', (event) => {"
        "  var port = cast.__platform__.PortConnector.bind('testport');"
        "  port.onmessage = (e) => {"
        "    var result = eval(e.data);"
        "    if (result && typeof(result.then) == 'function') {"
        "      result"
        "        .then(result =>"
        "                port.postMessage(valueOrUndefinedString(result)))"
        "        .catch(e => port.postMessage(JSON.stringify(e)));"
        "    } else {"
        "      port.postMessage(valueOrUndefinedString(result));"
        "    }"
        "  };"
        "});",
        "test"));
    binding_list.emplace_back(std::move(eval_js_binding));
    services_->api_bindings.set_bindings(std::move(binding_list));
  }

  void WaitQueryApiConnected() {
    EXPECT_FALSE(test_port_);
    test_port_ =
        services_->api_bindings.RunAndReturnConnectedPort("testport").Bind();
  }

  void OnComponentTeardown() {
    component_.reset();

    if (on_component_destroyed_) {
      std::move(on_component_destroyed_).Run();
    } else {
      ADD_FAILURE() << "Unexpected TestCastComponent teardown.";
    }
  }

  const sys::ServiceDirectory& test_realm_services_;

  // True if the Cast component should be offered a service directory channel
  // that has already been closed, to simulate the providing agent having
  // torn-down the directory before the component Connect()s through it.
  bool offer_closed_services_ = false;

  // False if the Cast component should not be offered any service directory.
  bool offer_services_ = true;

  // Holds the service directory and fake services offered to `component_`.
  std::optional<FakeComponentServices> services_;

  std::optional<fuchsia_component_support::DynamicComponentHost> component_;

  fuchsia::web::MessagePortPtr test_port_;

  base::OnceClosure on_component_destroyed_;
};

// Base class for all integration tests, parameterized on the set of
// "features" to enable in the `cast_runner` under test.
class CastRunnerIntegrationTest : public testing::Test {
 protected:
  CastRunnerIntegrationTest()
      : CastRunnerIntegrationTest(test::kCastRunnerFeaturesNone) {}
  explicit CastRunnerIntegrationTest(test::CastRunnerFeatures runner_features)
      : cast_runner_(runner_features) {}

  ~CastRunnerIntegrationTest() override = default;

  CastRunnerIntegrationTest(const CastRunnerIntegrationTest&) = delete;
  CastRunnerIntegrationTest& operator=(const CastRunnerIntegrationTest&) =
      delete;

  // testing::Test overrides.
  void SetUp() override {
    static constexpr std::string_view kTestServerRoot(
        "fuchsia_web/runners/cast/testdata");
    test_server_.ServeFilesFromSourceDirectory(kTestServerRoot);
    net::test_server::RegisterDefaultHandlers(&test_server_);
    ASSERT_TRUE(test_server_.Start());
  }

  // Returns the services exposed by the `CastRunnerLauncher` test Realm,
  // including those exposed by the `cast_runner` component under test.
  const sys::ServiceDirectory& test_realm_services() {
    return cast_runner_.exposed_services();
  }

  test::CastRunnerLauncher& cast_runner_launcher() { return cast_runner_; }

  // Returns the HTTP server used to serve fake content for Cast components.
  net::EmbeddedTestServer& test_server() { return test_server_; }

  // Convenience accessors for elements managed by the launcher.
  FakeApplicationConfigManager& app_config_manager() {
    return cast_runner_.fake_cast_agent().app_config_manager();
  }

 private:
  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::SingleThreadTaskEnvironment::MainThreadType::IO};

  // TODO(crbug.com/42050227): Override the RunLoop timeout set by
  // |task_environment_| to allow for the very high variability in web.Context
  // launch times.
  const base::test::ScopedRunLoopTimeout scoped_timeout_{
      FROM_HERE, TestTimeouts::action_max_timeout()};

  test::CastRunnerLauncher cast_runner_;
  net::EmbeddedTestServer test_server_;
};

}  // namespace

// A basic integration test ensuring a basic cast request launches the right
// URL in the Chromium service.
TEST_F(CastRunnerIntegrationTest, BasicRequest) {
  TestCastComponent component(test_realm_services());

  GURL app_url = test_server().GetURL(kBlankAppUrl);
  app_config_manager().AddApp(kTestAppId, app_url);
  component.StartCastComponentWithQueryApi();

  // Verify that the app has navigated to the expected URL.
  EXPECT_EQ(component.QueryAppUrl(), app_url.spec());
}

// Verify that the Runner can continue to be used even after its Context has
// crashed. Regression test for https://crbug.com/1066826.
// TODO(crbug.com/40682680): Replace this with a WebRunner test, ideally a
//   unit-test, which can simulate Context disconnection more simply.
TEST_F(CastRunnerIntegrationTest, CanRecreateContext) {
  TestCastComponent component(test_realm_services());
  const GURL app_url = test_server().GetURL(kBlankAppUrl);
  app_config_manager().AddApp(kTestAppId, app_url);

  // Create a Cast component and verify that it has loaded.
  component.StartCastComponentWithQueryApi(kTestAppId);

  // Connect to the CastRunner's Realm, and request to enumerate the contents
  // of the "web_instances" collection.
  fidl::SynchronousInterfacePtr<fuchsia::component::Realm> runner_realm;
  test_realm_services().Connect(
      runner_realm.NewRequest(),
      test::CastRunnerLauncher::kCastRunnerRealmProtocol);
  fidl::SynchronousInterfacePtr<fuchsia::component::ChildIterator>
      instance_iterator;
  fuchsia::component::Realm_ListChildren_Result list_children_result;
  zx_status_t status = runner_realm->ListChildren(
      fuchsia::component::decl::CollectionRef{.name = "web_instances"},
      instance_iterator.NewRequest(), &list_children_result);
  ASSERT_EQ(status, ZX_OK);
  ASSERT_FALSE(list_children_result.is_err());

  // The CastRunner's "web_instances" collection should contain exactly one
  // child component.
  std::vector<fuchsia::component::decl::ChildRef> web_instance_refs;
  status = instance_iterator->Next(&web_instance_refs);
  ASSERT_EQ(status, ZX_OK);
  ASSERT_EQ(web_instance_refs.size(), 1u);

  // Verify that no further children remain in "web_instances".
  std::vector<fuchsia::component::decl::ChildRef> empty_web_instance_refs;
  status = instance_iterator->Next(&empty_web_instance_refs);
  ASSERT_EQ(status, ZX_OK);
  ASSERT_TRUE(empty_web_instance_refs.empty());

  // Destroy the one child of the "web_instances" collection.
  fuchsia::component::Realm_DestroyChild_Result destroy_child_result;
  status = runner_realm->DestroyChild(std::move(web_instance_refs[0]),
                                      &destroy_child_result);
  ASSERT_EQ(status, ZX_OK);
  ASSERT_FALSE(destroy_child_result.is_err());

  // Expect that the Cast component goes away, since its container is gone.
  component.WaitForComponentDestroyed();

  // Create a second Cast component and verify that it has loaded.
  // There is no guarantee that the CastRunner has detected the old web.Context
  // disconnecting yet, so attempts to launch Cast components could fail.
  // WebContentRunner::CreateFrameWithParams() will synchronously verify that
  // the web.Context is not-yet-closed, to work-around that.
  TestCastComponent second_component(test_realm_services());
  second_component.StartCastComponentWithQueryApi(kTestAppId);
}

TEST_F(CastRunnerIntegrationTest, ApiBindings) {
  TestCastComponent component(test_realm_services());
  app_config_manager().AddApp(kTestAppId, test_server().GetURL(kBlankAppUrl));

  component.StartCastComponentWithQueryApi();

  // Verify that we can communicate with the query-API binding added by
  // `StartCastComponentWithQueryApi()`.
  EXPECT_EQ(component.ExecuteJavaScript("1+2+\"\""), "3");
}

TEST_F(CastRunnerIntegrationTest, UnknownCastAppId_Fails) {
  TestCastComponent component(test_realm_services());
  const char kUnknownComponentUrl[] = "cast:99999999";

  component.StartCastComponent(kUnknownComponentUrl);

  // Run the loop until the ComponentController is dropped.
  component.WaitForComponentDestroyed();
}

TEST_F(CastRunnerIntegrationTest, UrlRequestRewriteRulesProvider) {
  TestCastComponent component(test_realm_services());
  GURL echo_app_url = test_server().GetURL(kEchoHeaderPath);
  app_config_manager().AddApp(kTestAppId, echo_app_url);

  component.StartCastComponentWithQueryApi();

  EXPECT_EQ(component.ExecuteJavaScript("document.body.innerText"),
            "TestHeaderValue");
}

TEST_F(CastRunnerIntegrationTest, ApplicationControllerBound) {
  TestCastComponent component(test_realm_services());
  app_config_manager().AddApp(kTestAppId, test_server().GetURL(kBlankAppUrl));

  component.StartCastComponentWithQueryApi();

  // Run until the application calls SetApplicationController().
  component.application_context().WaitForSetApplicationController();
  EXPECT_TRUE(component.application_context().application_controller());
}

// Verify an App launched with remote debugging enabled is properly reachable.
TEST_F(CastRunnerIntegrationTest, RemoteDebugging) {
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL(kBlankAppUrl);
  auto app_config =
      FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url);
  app_config.set_enable_remote_debugging(true);
  app_config_manager().AddAppConfig(std::move(app_config));

  component.StartCastComponentWithQueryApi();

  // Connect to the debug service and ensure we get the proper response.
  base::Value::List devtools_list =
      GetDevToolsListFromPort(CastRunner::kRemoteDebuggingPort);
  EXPECT_EQ(devtools_list.size(), 1u);

  const auto* devtools_url = devtools_list[0].GetDict().FindString("url");
  ASSERT_TRUE(devtools_url);
  EXPECT_EQ(*devtools_url, app_url.spec());
}

TEST_F(CastRunnerIntegrationTest, IsolatedContext) {
  TestCastComponent component(test_realm_services());
  const GURL kContentDirectoryUrl("fuchsia-dir://testdata/empty.html");

  app_config_manager().AddAppConfig(
      CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl));
  component.StartCastComponentWithQueryApi();

  // Verify that the app was navigated to the isolated content URL.
  EXPECT_EQ(component.QueryAppUrl(), kContentDirectoryUrl.spec());
}

// Verify that the component fails to start if no service directory is offered.
TEST_F(CastRunnerIntegrationTest, ServiceDirectoryMissing_FailToStart) {
  TestCastComponent component(test_realm_services());
  component.disable_offer_services();
  app_config_manager().AddApp(kTestAppId,
                              test_server().GetURL(kEchoHeaderPath));

  component.StartCastComponent(base::StrCat({"cast:", kTestAppId}));

  // Expect that the component stops.
  component.WaitForComponentDestroyed();
}

// Verify that the component fails to start if the offered service directory
// channel has already been closed, such that Connect() calls will result in
// service request channels being dropped.
TEST_F(CastRunnerIntegrationTest, ServiceDirectoryEmpty_FailToStart) {
  TestCastComponent component(test_realm_services());
  component.offer_closed_services();
  app_config_manager().AddApp(kTestAppId,
                              test_server().GetURL(kEchoHeaderPath));

  component.StartCastComponent(base::StrCat({"cast:", kTestAppId}));

  // Expect that the component stops.
  component.WaitForComponentDestroyed();
}

// Simulate an Agent crash by tearing down `services_`, resulting in the
// service-directory and bindings passed to the Cast activity itself being
// closed. This should cause the component to terminate.
TEST_F(CastRunnerIntegrationTest, ServicesClose_TerminatesComponent) {
  TestCastComponent component(test_realm_services());
  app_config_manager().AddApp(kTestAppId,
                              test_server().GetURL(kEchoHeaderPath));

  component.StartCastComponentWithQueryApi();

  // Disconnect all service bindings.
  component.DisconnectServices();

  component.WaitForComponentDestroyed();
}

class AudioCastRunnerIntegrationTest : public CastRunnerIntegrationTest {
 public:
  AudioCastRunnerIntegrationTest()
      : CastRunnerIntegrationTest(
            test::kCastRunnerFeaturesFakeAudioDeviceEnumerator) {}
};

TEST_F(AudioCastRunnerIntegrationTest, Microphone) {
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL("/microphone.html");
  auto app_config =
      FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url);

  fuchsia::web::PermissionDescriptor mic_permission;
  mic_permission.set_type(fuchsia::web::PermissionType::MICROPHONE);
  app_config.mutable_permissions()->push_back(std::move(mic_permission));
  app_config_manager().AddAppConfig(std::move(app_config));

  // Expect fuchsia.media.Audio connection to be requested.
  base::RunLoop run_loop;
  cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure(
      fuchsia::media::Audio::Name_, run_loop.QuitClosure());

  component.StartCastComponentWithQueryApi();
  component.ExecuteJavaScript("connectMicrophone();");

  // Will quit once AudioCapturer is connected.
  run_loop.Run();
}

TEST_F(CastRunnerIntegrationTest, Camera) {
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL("/camera.html");
  auto app_config =
      FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url);

  fuchsia::web::PermissionDescriptor camera_permission;
  camera_permission.set_type(fuchsia::web::PermissionType::CAMERA);
  app_config.mutable_permissions()->push_back(std::move(camera_permission));
  app_config_manager().AddAppConfig(std::move(app_config));

  // Expect fuchsia.camera3.DeviceWatcher connection to be requested.
  cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure(
      fuchsia::camera3::DeviceWatcher::Name_,
      base::MakeExpectedRunAtLeastOnceClosure(FROM_HERE));

  component.StartCastComponentWithQueryApi();

  component.ExecuteJavaScript("connectCamera();");
}

TEST_F(CastRunnerIntegrationTest, CameraAccessAfterComponentShutdown) {
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL("/camera.html");

  // First app with camera permission.
  auto app_config =
      FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url);
  fuchsia::web::PermissionDescriptor camera_permission;
  camera_permission.set_type(fuchsia::web::PermissionType::CAMERA);
  app_config.mutable_permissions()->push_back(std::move(camera_permission));
  app_config_manager().AddAppConfig(std::move(app_config));

  // Second app without camera permission (but it will still try to access
  // fuchsia.camera3.DeviceWatcher service to enumerate devices).
  TestCastComponent second_component(test_realm_services());
  auto app_config_2 =
      FakeApplicationConfigManager::CreateConfig(kSecondTestAppId, app_url);
  app_config_manager().AddAppConfig(std::move(app_config_2));

  // Start and then shutdown the first app.
  component.StartCastComponentWithQueryApi(kTestAppId);
  component.ShutdownComponent();

  // Start the second app and try to connect the camera. It's expected to fail
  // to initialize the camera without crashing CastRunner.
  second_component.StartCastComponentWithQueryApi(kSecondTestAppId);
  EXPECT_EQ(second_component.ExecuteJavaScript("connectCamera();"),
            "getUserMediaFailed");
}

TEST_F(CastRunnerIntegrationTest, MultipleComponentsUsingCamera) {
  TestCastComponent first_component(test_realm_services());
  TestCastComponent second_component(test_realm_services());

  GURL app_url = test_server().GetURL("/camera.html");

  // Expect fuchsia.camera3.DeviceWatcher connection to be requested.
  cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure(
      fuchsia::camera3::DeviceWatcher::Name_,
      base::MakeExpectedRunAtLeastOnceClosure(FROM_HERE));

  // Start two apps, both with camera permission.
  auto app_config1 =
      FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url);
  fuchsia::web::PermissionDescriptor camera_permission1;
  camera_permission1.set_type(fuchsia::web::PermissionType::CAMERA);
  app_config1.mutable_permissions()->push_back(std::move(camera_permission1));
  app_config_manager().AddAppConfig(std::move(app_config1));
  first_component.StartCastComponentWithQueryApi(kTestAppId);

  auto app_config2 =
      FakeApplicationConfigManager::CreateConfig(kSecondTestAppId, app_url);
  fuchsia::web::PermissionDescriptor camera_permission2;
  camera_permission2.set_type(fuchsia::web::PermissionType::CAMERA);
  app_config2.mutable_permissions()->push_back(std::move(camera_permission2));
  app_config_manager().AddAppConfig(std::move(app_config2));
  second_component.StartCastComponentWithQueryApi(kSecondTestAppId);

  // Shut down the first component.
  first_component.ShutdownComponent();

  second_component.ExecuteJavaScript("connectCamera();");
}

class HeadlessCastRunnerIntegrationTest : public CastRunnerIntegrationTest {
 public:
  HeadlessCastRunnerIntegrationTest()
      : CastRunnerIntegrationTest(test::kCastRunnerFeaturesHeadless) {}
};

// A basic integration test ensuring a basic cast request launches the right
// URL in the Chromium service.
TEST_F(HeadlessCastRunnerIntegrationTest, Headless) {
  TestCastComponent component(test_realm_services());

  const char kAnimationPath[] = "/css_animation.html";
  const GURL animation_url = test_server().GetURL(kAnimationPath);
  app_config_manager().AddApp(kTestAppId, animation_url);

  component.StartCastComponentWithQueryApi();

  fuchsia::ui::views::ViewToken view_token;
  fuchsia::ui::views::ViewHolderToken view_holder_token;
  auto status =
      zx::eventpair::create(0u, &view_token.value, &view_holder_token.value);
  CHECK_EQ(ZX_OK, status);

  fuchsia::ui::views::ViewRefControl view_ref_control;
  fuchsia::ui::views::ViewRef view_ref;
  status = zx::eventpair::create(
      /*options*/ 0u, &view_ref_control.reference, &view_ref.reference);
  CHECK_EQ(ZX_OK, status);
  view_ref_control.reference.replace(
      ZX_DEFAULT_EVENTPAIR_RIGHTS & (~ZX_RIGHT_DUPLICATE),
      &view_ref_control.reference);
  view_ref.reference.replace(ZX_RIGHTS_BASIC, &view_ref.reference);

  // Create a view.
  auto view_provider = component.exposed_by_component()
                           .Connect<fuchsia::ui::app::ViewProvider>();
  view_provider->CreateViewWithViewRef(std::move(view_holder_token.value),
                                       std::move(view_ref_control),
                                       std::move(view_ref));

  component.api_bindings().RunAndReturnConnectedPort("animation_finished");

  // Verify that dropped "view" EventPair is handled properly.
  view_token.value.reset();
  component.api_bindings().RunAndReturnConnectedPort("view_hidden");
}

// Isolated *and* headless? Doesn't sound like much fun!
TEST_F(HeadlessCastRunnerIntegrationTest, IsolatedAndHeadless) {
  TestCastComponent component(test_realm_services());
  const GURL kContentDirectoryUrl("fuchsia-dir://testdata/empty.html");

  app_config_manager().AddAppConfig(
      CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl));
  component.StartCastComponentWithQueryApi();

  // Verify that the app was able to navigate to the isolated content URL.
  EXPECT_EQ(component.QueryAppUrl(), kContentDirectoryUrl.spec());
}

// Verifies that the Context can establish a connection to the Agent's
// MetricsRecorder service.
TEST_F(CastRunnerIntegrationTest, LegacyMetricsRedirect) {
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL(kBlankAppUrl);
  app_config_manager().AddApp(kTestAppId, app_url);

  bool connected_to_metrics_recorder_service = false;

  cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure(
      fuchsia::legacymetrics::MetricsRecorder::Name_,
      base::BindLambdaForTesting([&connected_to_metrics_recorder_service]() {
        connected_to_metrics_recorder_service = true;
      }));

  // If the Component is going to connect to the MetricsRecorder service, it
  // will have done so by the time the Component is responding.
  component.StartCastComponentWithQueryApi();
  ASSERT_EQ(connected_to_metrics_recorder_service,
#if BUILDFLAG(ENABLE_CAST_RECEIVER)
            true
#else
            false
#endif
  );
}

// Verifies that the ApplicationContext::OnApplicationTerminated() is notified
// with the component exit code if the web content closes itself.
TEST_F(CastRunnerIntegrationTest, OnApplicationTerminated_WindowClose) {
  TestCastComponent component(test_realm_services());
  const GURL url = test_server().GetURL(kBlankAppUrl);
  app_config_manager().AddApp(kTestAppId, url);

  component.StartCastComponentWithQueryApi();

  // It is possible to observe the component teardown before
  // OnApplicationTerminated() is received, so ignore that.
  component.SetIgnoreComponentDestroyed();

  // Have the web content close itself, and wait for OnApplicationTerminated().
  EXPECT_EQ(component.ExecuteJavaScript("window.close()"), "undefined");
  std::optional<zx_status_t> exit_code =
      component.application_context().WaitForApplicationTerminated();
  ASSERT_TRUE(exit_code);
  EXPECT_EQ(exit_code.value(), ZX_OK);
}

// Verifies that the ApplicationContext::OnApplicationTerminated() is notified
// with the component exit code if the component is requested to stop.
TEST_F(CastRunnerIntegrationTest, OnApplicationTerminated_ComponentStop) {
  TestCastComponent component(test_realm_services());
  const GURL url = test_server().GetURL(kBlankAppUrl);
  app_config_manager().AddApp(kTestAppId, url);

  component.StartCastComponentWithQueryApi();

  // It is possible to observe the component teardown before
  // OnApplicationTerminated() is received, so ignore that.
  component.SetIgnoreComponentDestroyed();

  // Request that the component be destroyed, and wait for
  // OnApplicationTerminated().
  component.ShutdownComponent();
  std::optional<zx_status_t> exit_code =
      component.application_context().WaitForApplicationTerminated();
  ASSERT_TRUE(exit_code);
  EXPECT_EQ(exit_code.value(), ZX_OK);
}

// Ensures that CastRunner handles the value not being specified.
// TODO(https://crrev.com/c/2516246): Check for no logging.
TEST_F(CastRunnerIntegrationTest, InitialMinConsoleLogSeverity_NotSet) {
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL(kBlankAppUrl);
  auto app_config =
      FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url);

  EXPECT_FALSE(app_config.has_initial_min_console_log_severity());
  app_config_manager().AddAppConfig(std::move(app_config));

  component.StartCastComponentWithQueryApi();
}

// TODO(https://crrev.com/c/2516246): Check for logging.
TEST_F(CastRunnerIntegrationTest, InitialMinConsoleLogSeverity_DEBUG) {
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL(kBlankAppUrl);
  auto app_config =
      FakeApplicationConfigManager::CreateConfig(kTestAppId, app_url);

  *app_config.mutable_initial_min_console_log_severity() =
      fuchsia::diagnostics::Severity::DEBUG;
  app_config_manager().AddAppConfig(std::move(app_config));

  component.StartCastComponentWithQueryApi();
}

TEST_F(CastRunnerIntegrationTest, WebGLContextAbsentWithoutVulkanFeature) {
  TestCastComponent component(test_realm_services());
  const char kTestPath[] = "/webgl_presence.html";
  const GURL test_url = test_server().GetURL(kTestPath);
  app_config_manager().AddApp(kTestAppId, test_url);

  component.StartCastComponentWithQueryApi();

  EXPECT_EQ(component.ExecuteJavaScript("document.title"), "absent");
}

TEST_F(CastRunnerIntegrationTest,
       WebGLContextAbsentWithoutVulkanFeature_IsolatedRunner) {
  TestCastComponent component(test_realm_services());
  const GURL kContentDirectoryUrl("fuchsia-dir://testdata/webgl_presence.html");

  app_config_manager().AddAppConfig(
      CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl));
  component.StartCastComponentWithQueryApi();

  EXPECT_EQ(component.ExecuteJavaScript("document.title"), "absent");
}

// Verifies that starting a component fails if CORS exempt headers cannot be
// fetched.
TEST_F(CastRunnerIntegrationTest, MissingCorsExemptHeaderProvider) {
  // Prevent the FakeCastAgent from publishing the
  // chromium.cast.CorsExemptHeaderProvider service.
  cast_runner_launcher().fake_cast_agent().RegisterOnConnectClosure(
      chromium::cast::CorsExemptHeaderProvider::Name_, base::DoNothing());

  // Start the Cast component, and wait for it to be destroyed.
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL(kBlankAppUrl);
  app_config_manager().AddApp(kTestAppId, app_url);
  component.StartCastComponent(base::StrCat({"cast:", kTestAppId}));

  // Expect it to be more or less immediately torn-down.
  component.WaitForComponentDestroyed();
}

// Verifies that CastRunner offers a chromium.cast.DataReset service.
// Verifies that after the DeletePersistentData() API is invoked, no further
// component-start requests are honoured.
// TODO(crbug.com/40730094): Expand the test to verify that the persisted data
// is correctly cleared (e.g. using a custom test HTML app that uses persisted
// data).
TEST_F(CastRunnerIntegrationTest, DataReset_Service) {
  base::RunLoop loop;
  auto data_reset = test_realm_services().Connect<chromium::cast::DataReset>();
  data_reset.set_error_handler([quit_loop = loop.QuitClosure()](zx_status_t) {
    quit_loop.Run();
    ADD_FAILURE();
  });
  bool succeeded = false;
  data_reset->DeletePersistentData([&succeeded, &loop](bool result) {
    succeeded = result;
    loop.Quit();
  });
  loop.Run();

  EXPECT_TRUE(succeeded);

  // Verify that it is no longer possible to launch a component.
  TestCastComponent component(test_realm_services());
  GURL app_url = test_server().GetURL(kBlankAppUrl);
  app_config_manager().AddApp(kTestAppId, app_url);
  component.StartCastComponent(base::StrCat({"cast:", kTestAppId}));
  component.WaitForComponentDestroyed();
}

// Verifies that the CastRunner exposes a fuchsia.web.FrameHost protocol
// capability, without requiring any special configuration.
TEST_F(CastRunnerIntegrationTest, FrameHost_Service) {
  // Connect to the fuchsia.web.FrameHost service and create a Frame.
  auto frame_host = test_realm_services().Connect<fuchsia::web::FrameHost>();
  fuchsia::web::FramePtr frame;
  frame_host->CreateFrameWithParams(fuchsia::web::CreateFrameParams(),
                                    frame.NewRequest());

  // Verify that a response is received for a LoadUrl() request to the frame.
  fuchsia::web::NavigationControllerPtr controller;
  frame->GetNavigationController(controller.NewRequest());
  const GURL url = test_server().GetURL(kBlankAppUrl);
  EXPECT_TRUE(LoadUrlAndExpectResponse(
      controller.get(), fuchsia::web::LoadUrlParams(), url.spec()));
}

// Check that connecting and disconnecting to the FrameHost service does not
// trigger shutdown of the devtools service.
TEST_F(CastRunnerIntegrationTest, FrameHostDebugging) {
  // Before triggering the launch of any `web_instance` by the `cast_runner`,
  // attach a `TestDebugListener`, to be notified when the DevTools port becomes
  // available.
  TestDebugListener dev_tools_listener;
  fidl::Binding<fuchsia::web::DevToolsListener> dev_tools_listener_binding(
      &dev_tools_listener);
  auto debug = test_realm_services().Connect<fuchsia::web::Debug>();
  debug.set_error_handler([](zx_status_t status) {
    ZX_LOG(ERROR, status) << "Failed to use debug protocol";
    ADD_FAILURE();
  });
  base::RunLoop dev_tools_enabled;
  debug->EnableDevTools(
      dev_tools_listener_binding.NewBinding(),
      [done = dev_tools_enabled.QuitClosure()]() { done.Run(); });
  dev_tools_enabled.Run();

  // Connect a `FrameHost` client, create a `Frame`, and navigate it to some
  // test content. Loading the content will result in the DevTools port becoming
  // available for the test to connect to.
  auto frame_host = test_realm_services().Connect<fuchsia::web::FrameHost>();
  fuchsia::web::CreateFrameParams create_frame_params;
  create_frame_params.set_enable_remote_debugging(true);
  auto frame = FrameForTest::Create(frame_host, std::move(create_frame_params));
  GURL url = test_server().GetURL("/defaultresponse");
  ASSERT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
                                       fuchsia::web::LoadUrlParams(),
                                       url.spec()));
  frame.navigation_listener().RunUntilUrlEquals(url);

  // Wait for the DevTools port to become available, and then connect to it to
  // verify that there is a single debuggable page listed.
  dev_tools_listener.RunUntilNumberOfPortsIs(1);
  uint16_t remote_debugging_port = *(dev_tools_listener.debug_ports().begin());

  base::Value::List devtools_list =
      GetDevToolsListFromPort(remote_debugging_port);
  EXPECT_EQ(devtools_list.size(), 1u);
  {
    const auto* devtools_url = devtools_list[0].GetDict().FindString("url");
    ASSERT_TRUE(devtools_url);
    EXPECT_EQ(*devtools_url, url);
  }

  // Create a new `FrameHost` client, and immediately close it. The DevTools
  // port should remain open regardless.
  auto frame_host_2 = test_realm_services().Connect<fuchsia::web::FrameHost>();
  frame_host_2.Unbind();

  // Navigate to a different page. The devtools service should still be active
  // and report the new page.
  GURL url2 = test_server().GetURL("/title1.html");
  ASSERT_TRUE(LoadUrlAndExpectResponse(frame.GetNavigationController(),
                                       fuchsia::web::LoadUrlParams(),
                                       url2.spec()));
  frame.navigation_listener().RunUntilUrlEquals(url2);

  devtools_list = GetDevToolsListFromPort(remote_debugging_port);
  EXPECT_EQ(devtools_list.size(), 1u);
  {
    const auto* devtools_url = devtools_list[0].GetDict().FindString("url");
    ASSERT_TRUE(devtools_url);
    EXPECT_EQ(*devtools_url, url2);
  }
}

#if defined(ARCH_CPU_ARM_FAMILY)
// TODO(crbug.com/42050537): Enable on ARM64 when bots support Vulkan.
#define MAYBE_VulkanCastRunnerIntegrationTest \
  DISABLED_VulkanCastRunnerIntegrationTest
#else
#define MAYBE_VulkanCastRunnerIntegrationTest VulkanCastRunnerIntegrationTest
#endif

class MAYBE_VulkanCastRunnerIntegrationTest : public CastRunnerIntegrationTest {
 public:
  MAYBE_VulkanCastRunnerIntegrationTest()
      : CastRunnerIntegrationTest(test::kCastRunnerFeaturesVulkan) {}
};

TEST_F(MAYBE_VulkanCastRunnerIntegrationTest,
       WebGLContextPresentWithVulkanFeature) {
  TestCastComponent component(test_realm_services());
  const char kTestPath[] = "/webgl_presence.html";
  const GURL test_url = test_server().GetURL(kTestPath);
  app_config_manager().AddApp(kTestAppId, test_url);

  component.StartCastComponentWithQueryApi();

  EXPECT_EQ(component.ExecuteJavaScript("document.title"), "present");
}

TEST_F(MAYBE_VulkanCastRunnerIntegrationTest,
       WebGLContextPresentWithVulkanFeature_IsolatedRunner) {
  TestCastComponent component(test_realm_services());
  const GURL kContentDirectoryUrl("fuchsia-dir://testdata/webgl_presence.html");

  app_config_manager().AddAppConfig(
      CreateAppConfigWithTestData(kTestAppId, kContentDirectoryUrl));
  component.StartCastComponentWithQueryApi();

  EXPECT_EQ(component.ExecuteJavaScript("document.title"), "present");
}