chromium/components/fuchsia_component_support/dynamic_component_host_unittest.cc

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

#include "components/fuchsia_component_support/dynamic_component_host.h"

#include <fuchsia/component/cpp/fidl_test_base.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/sys/cpp/service_directory.h>
#include <lib/vfs/cpp/pseudo_dir.h>
#include <lib/vfs/cpp/service.h>

#include <memory>
#include <utility>

#include "base/fuchsia/process_context.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
#include "base/test/task_environment.h"
#include "components/fuchsia_component_support/mock_realm.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace fuchsia_component_support {

namespace {

using testing::_;

MATCHER_P(EqCollectionRef, name, "") {
  return arg.name == name;
}

MATCHER_P2(EqChildDecl, name, url, "") {
  return arg.has_name() && arg.name() == name && arg.has_url() &&
         arg.url() == url;
}

MATCHER_P2(EqChildRef, name, collection, "") {
  return arg.name == name && arg.collection == collection;
}

// Verifies that `create_child_args` includes a dynamic offer for "/svc", and
// returns a channel connected to it, if so.
fidl::InterfaceHandle<fuchsia::io::Directory> GetSvcFromChildArgs(
    fuchsia::component::CreateChildArgs& create_child_args) {
  if (!create_child_args.has_dynamic_offers()) {
    return nullptr;
  }

  for (auto& offer : create_child_args.dynamic_offers()) {
    if (!offer.is_directory()) {
      continue;
    }

    const auto& directory_offer = offer.directory();
    if (!directory_offer.has_source_name() ||
        !directory_offer.has_target_name()) {
      return nullptr;
    }
    if (directory_offer.target_name() != "svc") {
      continue;
    }

    // Connect to the outgoing directory root, to use to look up the service
    // capability.
    fidl::InterfacePtr<fuchsia::io::Directory> root_dir;
    base::ComponentContextForProcess()->outgoing()->root_dir()->Serve(
        fuchsia::io::OpenFlags::RIGHT_READABLE |
            fuchsia::io::OpenFlags::RIGHT_WRITABLE |
            fuchsia::io::OpenFlags::DIRECTORY,
        root_dir.NewRequest().TakeChannel());

    // Determine the capability path, relative to the outgoing directory of
    // the calling process, and request to open it.
    // The channel will be closed as soon as the Open() call is processed,
    // if the path cannot be resolved.
    base::FilePath path(directory_offer.source_name());
    if (directory_offer.has_subdir()) {
      path = path.Append(directory_offer.subdir());
    }
    fidl::InterfaceHandle<fuchsia::io::Node> services_handle;
    root_dir->Open(fuchsia::io::OpenFlags::RIGHT_READABLE |
                       fuchsia::io::OpenFlags::RIGHT_WRITABLE |
                       fuchsia::io::OpenFlags::DIRECTORY,
                   {}, path.value(), services_handle.NewRequest());
    return fidl::InterfaceHandle<fuchsia::io::Directory>(
        services_handle.TakeChannel());
  }

  return nullptr;
}

bool HasPeerClosedHandle(
    const fidl::InterfaceHandle<fuchsia::io::Directory>& handle) {
  return handle.channel().wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time(),
                                   nullptr) != ZX_ERR_TIMED_OUT;
}

constexpr char kTestCollection[] = "test_collection";
constexpr char kTestChildId[] = "test-child-id";
constexpr char kTestComponentUrl[] = "dummy:url";

class DynamicComponentHostTest : public testing::Test {
 protected:
  DynamicComponentHostTest() : realm_(test_context_.additional_services()) {
    // By default simply reply indicating success, from Create/DestroyChild.
    ON_CALL(realm_, CreateChild)
        .WillByDefault(
            [](fuchsia::component::decl::CollectionRef,
               fuchsia::component::decl::Child,
               fuchsia::component::CreateChildArgs,
               fuchsia::component::Realm::CreateChildCallback callback) {
              callback({});
            });
    ON_CALL(realm_, DestroyChild)
        .WillByDefault(
            [](fuchsia::component::decl::ChildRef,
               fuchsia::component::Realm::DestroyChildCallback callback) {
              callback({});
            });

    // By default connect exposed directory requests to `exposed_`, to simplify
    // tests for exposed capabilities.
    ON_CALL(realm_, OpenExposedDir)
        .WillByDefault(
            [this](fuchsia::component::decl::ChildRef,
                   fidl::InterfaceRequest<fuchsia::io::Directory> exposed_dir,
                   fuchsia::component::Realm::OpenExposedDirCallback callback) {
              exposed_.Serve(fuchsia::io::OpenFlags::RIGHT_READABLE |
                                 fuchsia::io::OpenFlags::RIGHT_WRITABLE,
                             exposed_dir.TakeChannel());
              callback({});
            });
  }

  // Sets expectations on CreateChild(), OpenExposedDir() and DestroyChild()
  // being called, in that order, without expecting particular parameters.
  void ExpectCreateOpenAndDestroy() {
    testing::InSequence s;
    EXPECT_CALL(realm_, CreateChild(_, _, _, _));
    EXPECT_CALL(realm_, OpenExposedDir(_, _, _));
    EXPECT_CALL(realm_, DestroyChild(_, _));
  }

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

  base::TestComponentContextForProcess test_context_;

  testing::StrictMock<MockRealm> realm_;

  // Used to fake the "exposed dir" of the component.
  vfs::PseudoDir exposed_;
};

TEST_F(DynamicComponentHostTest, Basic) {
  ExpectCreateOpenAndDestroy();

  // Create and then immediately teardown the component.
  {
    DynamicComponentHost component(kTestCollection, kTestChildId,
                                   kTestComponentUrl, base::DoNothing(),
                                   nullptr);
  }

  // Spin the loop to allow the calls to reach `realm_`.
  base::RunLoop().RunUntilIdle();
}

TEST_F(DynamicComponentHostTest, CollectionAndChildName) {
  {
    testing::InSequence s;
    EXPECT_CALL(
        realm_,
        CreateChild(EqCollectionRef(kTestCollection),
                    EqChildDecl(kTestChildId, kTestComponentUrl), _, _));
    EXPECT_CALL(realm_, OpenExposedDir(
                            EqChildRef(kTestChildId, kTestCollection), _, _));
    EXPECT_CALL(realm_,
                DestroyChild(EqChildRef(kTestChildId, kTestCollection), _));
  }

  {
    DynamicComponentHost component(kTestCollection, kTestChildId,
                                   kTestComponentUrl, base::DoNothing(),
                                   nullptr);
  }

  // Spin the loop to allow the calls to reach `realm_`.
  base::RunLoop().RunUntilIdle();
}

TEST_F(DynamicComponentHostTest, OnTeardownCalledOnBinderClose) {
  ExpectCreateOpenAndDestroy();

  // Publish a fake Binder to the `exposed_` directory.
  fidl::InterfaceRequest<fuchsia::component::Binder> binder_request;
  exposed_.AddEntry(
      fuchsia::component::Binder::Name_,
      std::make_unique<vfs::Service>(
          [&binder_request](zx::channel request, async_dispatcher_t*) {
            binder_request = fidl::InterfaceRequest<fuchsia::component::Binder>(
                std::move(request));
          }));

  {
    DynamicComponentHost component(
        kTestCollection, kTestChildId, kTestComponentUrl,
        base::MakeExpectedRunClosure(FROM_HERE), nullptr);

    // Spin the loop to process calls to `realm_` and `exposed_`.
    base::RunLoop().RunUntilIdle();
    EXPECT_TRUE(binder_request);

    // Close `binder_request` and spin the loop to allow that to be observed.
    binder_request = nullptr;
    base::RunLoop().RunUntilIdle();
  }

  // Spin the loop to allow remaining calls to reach `realm_`.
  base::RunLoop().RunUntilIdle();
}

TEST_F(DynamicComponentHostTest,
       OnTeardownNotCalledIfDestroyedBeforeBinderClose) {
  ExpectCreateOpenAndDestroy();

  // Create and immediately teardown the component, so that Binder teardown
  // will not be observed until after the DynamicComponentHost has gone.
  {
    DynamicComponentHost component(
        kTestCollection, kTestChildId, kTestComponentUrl,
        base::MakeExpectedNotRunClosure(FROM_HERE), nullptr);
  }

  // Spin the loop to allow remaining calls to reach `realm_`.
  base::RunLoop().RunUntilIdle();
}

TEST_F(DynamicComponentHostTest, WithoutServiceDirectory) {
  // Capture the `CreateChildArgs` from the `Realm.CreateChild()` call.
  fuchsia::component::CreateChildArgs create_child_args;
  {
    testing::InSequence s;
    EXPECT_CALL(realm_, CreateChild(_, _, _, _))
        .WillOnce([&create_child_args](
                      fuchsia::component::decl::CollectionRef,
                      fuchsia::component::decl::Child,
                      fuchsia::component::CreateChildArgs args,
                      fuchsia::component::Realm::CreateChildCallback callback) {
          create_child_args = std::move(args);
          callback({});
        });
    EXPECT_CALL(realm_, OpenExposedDir(_, _, _));
    EXPECT_CALL(realm_, DestroyChild(_, _));
  }

  {
    DynamicComponentHost component(kTestCollection, kTestChildId,
                                   kTestComponentUrl, base::DoNothing(),
                                   nullptr);

    // Spin the event loop to process the `CreateChild()` call.
    base::RunLoop().RunUntilIdle();

    // Verify that no "svc" directory is offered in the `CreateChildArgs`.
    fidl::InterfaceHandle<fuchsia::io::Directory> svc_handle =
        GetSvcFromChildArgs(create_child_args);
    EXPECT_FALSE(svc_handle);
  }

  // Spin the loop to allow the teardown calls to reach `realm_`.
  base::RunLoop().RunUntilIdle();
}

TEST_F(DynamicComponentHostTest, WithServiceDirectory) {
  // Capture the `CreateChildArgs` from the `Realm.CreateChild()` call.
  fuchsia::component::CreateChildArgs create_child_args;
  {
    testing::InSequence s;
    EXPECT_CALL(realm_, CreateChild(_, _, _, _))
        .WillOnce([&create_child_args](
                      fuchsia::component::decl::CollectionRef,
                      fuchsia::component::decl::Child,
                      fuchsia::component::CreateChildArgs args,
                      fuchsia::component::Realm::CreateChildCallback callback) {
          create_child_args = std::move(args);
          callback({});
        });
    EXPECT_CALL(realm_, OpenExposedDir(_, _, _));
    EXPECT_CALL(realm_, DestroyChild(_, _));
  }

  {
    // Create a directory handle for the service directory.
    fidl::InterfaceHandle<fuchsia::io::Directory> handle;
    vfs::PseudoDir service_directory;
    service_directory.Serve(fuchsia::io::OpenFlags::RIGHT_READABLE |
                                fuchsia::io::OpenFlags::RIGHT_WRITABLE,
                            handle.NewRequest().TakeChannel());

    DynamicComponentHost component(kTestCollection, kTestChildId,
                                   kTestComponentUrl, base::DoNothing(),
                                   std::move(handle));

    // Spin the event loop to process the `CreateChild()` call.
    base::RunLoop().RunUntilIdle();

    // Verify that a "svc" directory was offered in the `CreateChildArgs`.
    fidl::InterfaceHandle<fuchsia::io::Directory> svc_handle =
        GetSvcFromChildArgs(create_child_args);
    EXPECT_TRUE(svc_handle);

    // Spin the event loop to allow the Open() of the directory attempted by
    // GetSvcFromChildArgs() to be processed, then verify that the `svc_handle`
    // was not closed by the peer.
    base::RunLoop().RunUntilIdle();
    EXPECT_FALSE(HasPeerClosedHandle(svc_handle));
  }

  // Spin the loop to allow teardown calls to reach `realm_`.
  base::RunLoop().RunUntilIdle();

  // Verify that the "svc" directory offered in the `CreateChildArgs` is no
  // longer available after the `DynamicComponentHost` has been destroyed.
  fidl::InterfaceHandle<fuchsia::io::Directory> svc_handle =
      GetSvcFromChildArgs(create_child_args);
  EXPECT_TRUE(svc_handle);

  // Spin the loop to allow the Open() of the "svc" directory to be processed.
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(HasPeerClosedHandle(svc_handle));
}

}  // namespace

}  // namespace fuchsia_component_support