chromium/fuchsia_web/webengine/web_engine_debug_integration_test.cc

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

#include <fuchsia/web/cpp/fidl.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/fidl/cpp/binding_set.h>

#include "base/containers/contains.h"
#include "base/fuchsia/file_utils.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "fuchsia_web/common/test/fit_adapter.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/webengine/test/context_provider_for_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

const char kTestServerRoot[] = "fuchsia_web/webengine/test/data";

}  // namespace

class WebEngineDebugIntegrationTest : public testing::Test {
 public:
  WebEngineDebugIntegrationTest()
      : web_context_provider_(ContextProviderForDebugTest::Create(
            base::CommandLine(base::CommandLine::NO_PROGRAM))),
        dev_tools_listener_binding_(&dev_tools_listener_) {
    web_context_provider_.ptr().set_error_handler(
        [](zx_status_t status) { FAIL() << zx_status_get_string(status); });
  }

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

  ~WebEngineDebugIntegrationTest() override = default;

  void SetUp() override {
    ASSERT_NO_FATAL_FAILURE(
        web_context_provider_.ConnectToDebug(debug_.NewRequest()));

    // Attach the DevToolsListener. EnableDevTools has an acknowledgement
    // callback so the listener will have been added after this call returns.
    debug_->EnableDevTools(dev_tools_listener_binding_.NewBinding());

    test_server_.ServeFilesFromSourceDirectory(kTestServerRoot);
    ASSERT_TRUE(test_server_.Start());
  }

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

  ContextProviderForDebugTest web_context_provider_;
  TestDebugListener dev_tools_listener_;
  fidl::Binding<fuchsia::web::DevToolsListener> dev_tools_listener_binding_;
  fuchsia::web::DebugSyncPtr debug_;

  base::OnceClosure on_url_fetch_complete_ack_;

  net::EmbeddedTestServer test_server_;
};

enum class UserModeDebugging { kEnabled = 0, kDisabled = 1 };

// Helper struct to intiialize all data necessary for a Context to create a
// Frame and navigate it to a specific URL.
struct TestContextAndFrame {
  explicit TestContextAndFrame(fuchsia::web::ContextProvider* context_provider,
                               UserModeDebugging user_mode_debugging,
                               std::string url) {
    // Create a Context, a Frame and navigate it to |url|.
    auto directory =
        base::OpenDirectoryHandle(base::FilePath(base::kServiceDirectoryPath));
    if (!directory.is_valid())
      return;

    fuchsia::web::CreateContextParams create_params;
    create_params.set_features(fuchsia::web::ContextFeatureFlags::NETWORK);
    create_params.set_service_directory(std::move(directory));
    if (user_mode_debugging == UserModeDebugging::kEnabled)
      create_params.set_remote_debugging_port(0);
    context_provider->Create(std::move(create_params), context.NewRequest());
    context->CreateFrame(frame.NewRequest());
    frame->GetNavigationController(controller.NewRequest());
    if (!LoadUrlAndExpectResponse(controller.get(),
                                  fuchsia::web::LoadUrlParams(), url)) {
      ADD_FAILURE();
      context.Unbind();
      frame.Unbind();
      controller.Unbind();
      return;
    }
  }

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

  ~TestContextAndFrame() = default;

  fuchsia::web::ContextPtr context;
  fuchsia::web::FramePtr frame;
  fuchsia::web::NavigationControllerPtr controller;
};

// Test the Debug service is properly started and accessible.
TEST_F(WebEngineDebugIntegrationTest, DebugService) {
  std::string url = test_server_.GetURL("/title1.html").spec();
  TestContextAndFrame frame_data(web_context_provider_.get(),
                                 UserModeDebugging::kDisabled, url);
  ASSERT_TRUE(frame_data.context);

  // Test the debug information is correct.
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(1u));

  base::Value::List devtools_list =
      GetDevToolsListFromPort(*dev_tools_listener_.debug_ports().begin());
  EXPECT_EQ(devtools_list.size(), 1u);

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

  const auto* devtools_title = devtools_dict.FindString("title");
  ASSERT_TRUE(devtools_title);
  EXPECT_EQ(*devtools_title, "title 1");

  // Unbind the context and wait for the listener to no longer have any active
  // DevTools port.
  frame_data.context.Unbind();
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(0));
}

TEST_F(WebEngineDebugIntegrationTest, MultipleDebugClients) {
  std::string url1 = test_server_.GetURL("/title1.html").spec();
  TestContextAndFrame frame_data1(web_context_provider_.get(),
                                  UserModeDebugging::kDisabled, url1);
  ASSERT_TRUE(frame_data1.context);

  // Test the debug information is correct.
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(1u));
  uint16_t port1 = *dev_tools_listener_.debug_ports().begin();

  base::Value::List devtools_list1 = GetDevToolsListFromPort(port1);
  EXPECT_EQ(devtools_list1.size(), 1u);

  const auto& devtools_dict1 = devtools_list1[0].GetDict();
  const auto* devtools_url1 = devtools_dict1.FindString("url");
  ASSERT_TRUE(devtools_url1);
  EXPECT_EQ(*devtools_url1, url1);

  const auto* devtools_title1 = devtools_dict1.FindString("title");
  ASSERT_TRUE(devtools_title1);
  EXPECT_EQ(*devtools_title1, "title 1");

  // Connect a second Debug interface.
  fuchsia::web::DebugSyncPtr debug2;
  ASSERT_NO_FATAL_FAILURE(
      web_context_provider_.ConnectToDebug(debug2.NewRequest()));
  TestDebugListener dev_tools_listener2;
  fidl::Binding<fuchsia::web::DevToolsListener> dev_tools_listener_binding2(
      &dev_tools_listener2);
  debug2->EnableDevTools(dev_tools_listener_binding2.NewBinding());

  // Create a second Context, a second Frame and navigate it to title2.html.
  std::string url2 = test_server_.GetURL("/title2.html").spec();
  TestContextAndFrame frame_data2(web_context_provider_.get(),
                                  UserModeDebugging::kDisabled, url2);
  ASSERT_TRUE(frame_data2.context);

  // Ensure each DevTools listener has the right information.
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(2u));
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener2.RunUntilNumberOfPortsIs(1u));

  uint16_t port2 = *dev_tools_listener2.debug_ports().begin();
  ASSERT_NE(port1, port2);
  ASSERT_TRUE(base::Contains(dev_tools_listener_.debug_ports(), port2));

  base::Value::List devtools_list2 = GetDevToolsListFromPort(port2);
  EXPECT_EQ(devtools_list2.size(), 1u);

  const auto& devtools_dict2 = devtools_list2[0].GetDict();
  const auto* devtools_url2 = devtools_dict2.FindString("url");
  ASSERT_TRUE(devtools_url2);
  EXPECT_EQ(*devtools_url2, url2);

  const auto* devtools_title2 = devtools_dict2.FindString("title");
  ASSERT_TRUE(devtools_title2);
  EXPECT_EQ(*devtools_title2, "title 2");

  // Unbind the first Context, each listener should still have one open port.
  frame_data1.context.Unbind();
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(1u));
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener2.RunUntilNumberOfPortsIs(1u));

  // Unbind the second Context, no listener should have any open port.
  frame_data2.context.Unbind();
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(0));
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener2.RunUntilNumberOfPortsIs(0));
}

// Test the Debug service is accessible when the User service is requested.
TEST_F(WebEngineDebugIntegrationTest, DebugAndUserService) {
  std::string url = test_server_.GetURL("/title1.html").spec();
  TestContextAndFrame frame_data(web_context_provider_.get(),
                                 UserModeDebugging::kEnabled, url);
  ASSERT_TRUE(frame_data.context);

  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(1u));

  // Check we are getting the same port on both the debug and user APIs.
  base::test::TestFuture<fuchsia::web::Context_GetRemoteDebuggingPort_Result>
      port_receiver;
  frame_data.context->GetRemoteDebuggingPort(
      CallbackToFitFunction(port_receiver.GetCallback()));
  ASSERT_TRUE(port_receiver.Wait());

  ASSERT_TRUE(port_receiver.Get().is_response());
  uint16_t remote_debugging_port = port_receiver.Get().response().port;
  ASSERT_EQ(remote_debugging_port, *dev_tools_listener_.debug_ports().begin());

  // Test the debug information is correct.
  base::Value::List devtools_list =
      GetDevToolsListFromPort(remote_debugging_port);
  EXPECT_EQ(devtools_list.size(), 1u);

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

  const auto* devtools_title = devtools_dict.FindString("title");
  ASSERT_TRUE(devtools_title);
  EXPECT_EQ(*devtools_title, "title 1");

  // Unbind the context and wait for the listener to no longer have any active
  // DevTools port.
  frame_data.context.Unbind();
  ASSERT_NO_FATAL_FAILURE(dev_tools_listener_.RunUntilNumberOfPortsIs(0));
}