chromium/chrome/browser/ash/crosapi/test_mojo_connection_manager_unittest.cc

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

#include "chrome/browser/ash/crosapi/test_mojo_connection_manager.h"

#include <fcntl.h>

#include <memory>
#include <utility>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_path_watcher.h"
#include "base/files/scoped_file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/process/launch.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/multiprocess_test.h"
#include "base/test/task_environment.h"
#include "base/test/test_timeouts.h"
#include "chrome/browser/ash/crosapi/browser_service_host_ash.h"
#include "chrome/browser/ash/crosapi/browser_service_host_observer.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/idle_service_ash.h"
#include "chrome/browser/ash/crosapi/test_crosapi_dependency_registry.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/system/fake_statistics_provider.h"
#include "chromeos/crosapi/cpp/crosapi_constants.h"
#include "chromeos/crosapi/mojom/crosapi.mojom.h"
#include "chromeos/startup/startup_switches.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/fake_user_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "mojo/public/cpp/platform/socket_utils_posix.h"
#include "mojo/public/cpp/system/invitation.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/multiprocess_func_list.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.h"

namespace crosapi {
namespace {

class TestBrowserService : public crosapi::mojom::BrowserService {
 public:
  TestBrowserService() : receiver_(this) {}
  ~TestBrowserService() override = default;

  mojo::PendingRemote<crosapi::mojom::BrowserService>
  BindNewPipeAndPassRemote() {
    return receiver_.BindNewPipeAndPassRemote();
  }

  void REMOVED_0() override { NOTIMPLEMENTED(); }
  void REMOVED_2() override { NOTIMPLEMENTED(); }
  void REMOVED_7(bool should_trigger_session_restore,
                 base::OnceClosure callback) override {
    NOTIMPLEMENTED();
  }
  void REMOVED_16(
      base::flat_map<policy::PolicyNamespace, std::vector<uint8_t>>) override {
    NOTIMPLEMENTED();
  }

  void NewWindow(bool incognito,
                 bool should_trigger_session_restore,
                 int64_t target_display_id,
                 std::optional<uint64_t> profile_id,
                 NewWindowCallback callback) override {}
  void NewWindowForDetachingTab(
      const std::u16string& tab_id,
      const std::u16string& group_id,
      NewWindowForDetachingTabCallback closure) override {}
  void NewFullscreenWindow(const GURL& url,
                           int64_t target_display_id,
                           NewFullscreenWindowCallback callback) override {}
  void NewGuestWindow(int64_t target_display_id,
                      NewGuestWindowCallback callback) override {}
  void NewTab(std::optional<uint64_t> profile_id,
              NewTabCallback callback) override {}
  void Launch(int64_t target_display_id,
              std::optional<uint64_t> profile_id,
              LaunchCallback callback) override {}
  void OpenUrl(const GURL& url,
               crosapi::mojom::OpenUrlParamsPtr params,
               OpenUrlCallback callback) override {}
  void RestoreTab(RestoreTabCallback callback) override {}
  void HandleTabScrubbing(float x_offset, bool is_fling_scroll_event) override {
  }
  void GetFeedbackData(GetFeedbackDataCallback callback) override {}
  void GetHistograms(GetHistogramsCallback callback) override {}
  void GetActiveTabUrl(GetActiveTabUrlCallback callback) override {}
  void UpdateDeviceAccountPolicy(const std::vector<uint8_t>& policy) override {}
  void NotifyPolicyFetchAttempt() override {}
  void UpdateKeepAlive(bool enabled) override {}
  void OpenForFullRestore(bool skip_crash_restore) override {}
  void OpenProfileManager() override {}
  void UpdateComponentPolicy(
      base::flat_map<policy::PolicyNamespace, base::Value> policy) override {}
  void OpenCaptivePortalSignin(const GURL& url,
                               OpenUrlCallback callback) override {}

 private:
  mojo::Receiver<mojom::BrowserService> receiver_;
};

class TestBrowserServiceHostObserver : public BrowserServiceHostObserver {
 public:
  // |callback| is invoked when OnBrowserServiceConnected is called
  // |num_calls| times.
  TestBrowserServiceHostObserver(size_t num_calls, base::OnceClosure callback)
      : remaining_num_calls_(num_calls), callback_(std::move(callback)) {
    DCHECK_GT(num_calls, 0u);
  }

  void OnBrowserServiceConnected(CrosapiId id,
                                 mojo::RemoteSetElementId mojo_id,
                                 mojom::BrowserService* browser_service,
                                 uint32_t browser_service_version) override {
    --remaining_num_calls_;
    if (remaining_num_calls_ == 0)
      std::move(callback_).Run();
  }

 private:
  size_t remaining_num_calls_;
  base::OnceClosure callback_;
};

std::vector<base::ScopedFD> ConnectTestingMojoSocket(
    const std::string& socket_path) {
  auto channel = mojo::NamedPlatformChannel::ConnectToServer(socket_path);
  if (!channel.is_valid())
    return {};
  base::ScopedFD socket_fd = channel.TakePlatformHandle().TakeFD();

  uint8_t buf[32];
  std::vector<base::ScopedFD> descriptors;
  ssize_t size;
  base::RunLoop run_loop;
  base::ThreadPool::PostTaskAndReply(
      FROM_HERE, {base::MayBlock()}, base::BindLambdaForTesting([&]() {
        // Mark the channel as blocking.
        int flags = fcntl(socket_fd.get(), F_GETFL);
        PCHECK(flags != -1);
        fcntl(socket_fd.get(), F_SETFL, flags & ~O_NONBLOCK);
        size = mojo::SocketRecvmsg(socket_fd.get(), buf, sizeof(buf),
                                   &descriptors, true /*block*/);
      }),
      run_loop.QuitClosure());
  run_loop.Run();
  EXPECT_EQ(1, size);
  EXPECT_EQ(1u, buf[0]);
  return descriptors;
}

base::Process LaunchTestSubprocess(std::vector<base::ScopedFD> descriptors) {
  base::LaunchOptions options;
  options.fds_to_remap.emplace_back(descriptors[0].get(), descriptors[0].get());
  options.fds_to_remap.emplace_back(descriptors[1].get(), descriptors[1].get());
  base::CommandLine cmd(base::GetMultiProcessTestChildBaseCommandLine());
  cmd.AppendSwitchASCII(chromeos::switches::kCrosStartupDataFD,
                        base::NumberToString(descriptors[0].get()));
  cmd.AppendSwitchASCII(kCrosapiMojoPlatformChannelHandle,
                        base::NumberToString(descriptors[1].get()));
  return base::SpawnMultiProcessTestChild("CrosapiClientMain", cmd, options);
}

}  // namespace

using TestMojoConnectionManagerTest = testing::Test;

TEST_F(TestMojoConnectionManagerTest, ConnectMultipleClients) {
  // Set fake statistics provider which is needed by crosapi_util.
  ash::system::FakeStatisticsProvider statistics_provider_;
  ash::system::StatisticsProvider::SetTestProvider(&statistics_provider_);

  // Create temp dir before task environment, just in case lingering tasks need
  // to access it.
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  // IdleServiceAsh uses objects that are unavailable for this test. Disable it
  // to avoid problems.
  crosapi::IdleServiceAsh::DisableForTesting();

  // Use IO type to support the FileDescriptorWatcher API on POSIX.
  // TestingProfileManager instantiated below requires a TaskRunner.
  content::BrowserTaskEnvironment task_environment{
      base::test::TaskEnvironment::MainThreadType::IO};

  ash::LoginState::Initialize();
  absl::Cleanup login_state_teardown = &ash::LoginState::Shutdown;

  // Constructing CrosapiManager requires ProfileManager.
  // Also, constructing BrowserInitParams requires local state prefs.
  TestingProfileManager testing_profile_manager(
      TestingBrowserProcess::GetGlobal());
  ASSERT_TRUE(testing_profile_manager.SetUp());

  // Set up UserManager to fake the login state.
  user_manager::FakeUserManager user_manager(
      TestingBrowserProcess::GetGlobal()->local_state());
  user_manager.Initialize();
  absl::Cleanup user_manager_teardown = [&user_manager] {
    user_manager.Destroy();
  };
  const AccountId account = AccountId::FromUserEmail("test@test");
  const user_manager::User* user = user_manager.AddUser(account);
  user_manager.UserLoggedIn(account, user->username_hash(), false, false);
  TestingProfile* profile =
      testing_profile_manager.CreateTestingProfile(account.GetUserEmail());
  profile->set_profile_name(account.GetUserEmail());
  user_manager.OnUserProfileCreated(account, profile->GetPrefs());

  auto crosapi_manager = CreateCrosapiManagerWithTestRegistry();

  // Ash-chrome queues an invitation, drop a socket and wait for connection.
  std::string socket_path =
      temp_dir.GetPath().MaybeAsASCII() + "/lacros.socket";

  // Sets up watcher to Wait for the socket to be created by
  // |TestMojoConnectionManager| before attempting to connect. There is no
  // garanteen that |OnTestingSocketCreated| has run after the run loop is done,
  // so this test should NOT depend on the assumption.
  base::FilePathWatcher watcher;
  base::RunLoop run_loop1;
  watcher.Watch(base::FilePath(socket_path),
                base::FilePathWatcher::Type::kNonRecursive,
                base::BindRepeating(base::BindLambdaForTesting(
                    [&run_loop1](const base::FilePath& path, bool error) {
                      EXPECT_FALSE(error);
                      run_loop1.Quit();
                    })));
  TestMojoConnectionManager test_mojo_connection_manager{
      base::FilePath(socket_path)};
  run_loop1.Run();

  // Test connects with ash-chrome via the socket.
  std::vector<base::ScopedFD> descriptors1 =
      ConnectTestingMojoSocket(socket_path);
  ASSERT_EQ(2u, descriptors1.size());
  std::vector<base::ScopedFD> descriptors2 =
      ConnectTestingMojoSocket(socket_path);
  ASSERT_EQ(2u, descriptors2.size());

  base::RunLoop run_loop2;
  // Two BrowserService connections should be made (one for each subprocess).
  TestBrowserServiceHostObserver observer(2, run_loop2.QuitClosure());
  crosapi_manager->crosapi_ash()->browser_service_host_ash()->AddObserver(
      &observer);
  absl::Cleanup observer_reset =
      [&observer, &crosapi_manager] {
        crosapi_manager->crosapi_ash()
            ->browser_service_host_ash()
            ->RemoveObserver(&observer);
      };

  // Then launch two subprocesses running in parallel.
  // These will connect BrowserService mojo.
  base::Process sub1 = LaunchTestSubprocess(std::move(descriptors1));
  base::Process sub2 = LaunchTestSubprocess(std::move(descriptors2));

  // Wait for two BrowserService instances.
  run_loop2.Run();

  // Connection succeeded. Terminate subprocesses.
  ASSERT_TRUE(base::TerminateMultiProcessTestChild(sub1, 0, true));
  sub1.Close();
  ASSERT_TRUE(base::TerminateMultiProcessTestChild(sub2, 0, true));
  sub2.Close();

  user_manager.OnUserProfileWillBeDestroyed(account);
}

// Another process that emulates the behavior of lacros-chrome.
MULTIPROCESS_TEST_MAIN(CrosapiClientMain) {
  base::test::SingleThreadTaskEnvironment task_environment;

  mojo::IncomingInvitation invitation = mojo::IncomingInvitation::Accept(
      mojo::PlatformChannel::RecoverPassedEndpointFromString(
          base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
              kCrosapiMojoPlatformChannelHandle)));
  base::RunLoop run_loop;
  mojo::Remote<crosapi::mojom::Crosapi> crosapi(
      mojo::PendingRemote<crosapi::mojom::Crosapi>(
          invitation.ExtractMessagePipe(0), 0u));
  mojo::Remote<crosapi::mojom::BrowserServiceHost> browser_service_host;
  crosapi->BindBrowserServiceHost(
      browser_service_host.BindNewPipeAndPassReceiver());
  TestBrowserService test_browser_service;
  browser_service_host->AddBrowserService(
      test_browser_service.BindNewPipeAndPassRemote());
  // Do not return from the run loop.
  run_loop.Run();
  return 0;
}

}  // namespace crosapi