chromium/chromeos/ash/components/drivefs/drivefs_session_unittest.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 "chromeos/ash/components/drivefs/drivefs_session.h"

#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_move_support.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/timer/mock_timer.h"
#include "chromeos/ash/components/disks/disk_mount_manager.h"
#include "chromeos/ash/components/disks/mock_disk_mount_manager.h"
#include "chromeos/ash/components/drivefs/fake_drivefs.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom-test-utils.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace drivefs {
namespace {

constexpr char kExpectedMountDir[] = "drivefs-salt-g-ID";
constexpr char kExpectedMountPath[] = "/media/drivefsroot/mountdir";
constexpr char kExpectedDataDir[] = "/path/to/profile/GCache/v2/salt-g-ID";
constexpr char kExpectedMyFilesDir[] = "/path/to/profile/MyFiles";

static const std::optional<base::TimeDelta> kEmptyDelay;
static const std::optional<base::TimeDelta> kDefaultDelay = base::Seconds(5);

using testing::_;
using testing::Invoke;
using testing::InvokeWithoutArgs;
using MountFailure = DriveFsSession::MountObserver::MountFailure;

class DriveFsDiskMounterTest : public testing::Test {
 protected:
  std::string StartMount(DiskMounter* mounter) {
    auto token = base::UnguessableToken::Create();
    std::string source;
    EXPECT_CALL(
        disk_manager_,
        MountPath(testing::StartsWith("drivefs://"), "", kExpectedMountDir,
                  testing::AllOf(
                      testing::Contains(
                          "datadir=/path/to/profile/GCache/v2/salt-g-ID"),
                      testing::Contains("myfiles=/path/to/profile/MyFiles")),
                  _, ash::MountAccessMode::kReadWrite, _))
        .WillOnce(testing::DoAll(testing::SaveArg<0>(&source),
                                 MoveArg<6>(&mount_callback_)));

    mounter->Mount(token, base::FilePath(kExpectedDataDir),
                   base::FilePath(kExpectedMyFilesDir), kExpectedMountDir,
                   base::BindOnce(&DriveFsDiskMounterTest::OnCompleted,
                                  base::Unretained(this)));
    testing::Mock::VerifyAndClear(&disk_manager_);
    return source.substr(strlen("drivefs://"));
  }

  MOCK_METHOD1(OnCompleted, void(base::FilePath));

  base::test::TaskEnvironment task_environment_;
  ash::disks::MockDiskMountManager disk_manager_;
  ash::disks::DiskMountManager::MountPathCallback mount_callback_;
};

TEST_F(DriveFsDiskMounterTest, MountUnmount) {
  auto mounter = DiskMounter::Create(&disk_manager_);
  auto token = StartMount(mounter.get());
  base::RunLoop run_loop;
  EXPECT_CALL(*this, OnCompleted(base::FilePath(kExpectedMountPath)))
      .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
  std::move(mount_callback_)
      .Run(ash::MountError::kSuccess,
           {base::StrCat({"drivefs://", token}), kExpectedMountPath,
            ash::MountType::kNetworkStorage});
  run_loop.Run();

  EXPECT_CALL(disk_manager_, UnmountPath(kExpectedMountPath, _));
  mounter.reset();
}

TEST_F(DriveFsDiskMounterTest, DestroyAfterMounted) {
  auto mounter = DiskMounter::Create(&disk_manager_);
  auto token = StartMount(mounter.get());
  base::RunLoop run_loop;
  EXPECT_CALL(*this, OnCompleted(base::FilePath(kExpectedMountPath)))
      .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
  std::move(mount_callback_)
      .Run(ash::MountError::kSuccess,
           {base::StrCat({"drivefs://", token}), kExpectedMountPath,
            ash::MountType::kNetworkStorage});
  run_loop.Run();

  EXPECT_CALL(disk_manager_, UnmountPath(kExpectedMountPath, _));
}

TEST_F(DriveFsDiskMounterTest, DestroyBeforeMounted) {
  EXPECT_CALL(disk_manager_, UnmountPath(_, _)).Times(0);
  auto mounter = DiskMounter::Create(&disk_manager_);
  StartMount(mounter.get());
}

TEST_F(DriveFsDiskMounterTest, MountError) {
  EXPECT_CALL(disk_manager_, UnmountPath(_, _)).Times(0);

  auto mounter = DiskMounter::Create(&disk_manager_);
  auto token = StartMount(mounter.get());
  base::RunLoop run_loop;
  EXPECT_CALL(*this, OnCompleted(base::FilePath()))
      .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));
  std::move(mount_callback_)
      .Run(ash::MountError::kInvalidMountOptions,
           {base::StrCat({"drivefs://", token}), kExpectedMountPath,
            ash::MountType::kNetworkStorage});
  run_loop.Run();
}

class MockDiskMounter : public DiskMounter {
 public:
  MockDiskMounter() { ++gInstanceCounter; }

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

  ~MockDiskMounter() override { --gInstanceCounter; }

  void Mount(const base::UnguessableToken& token,
             const base::FilePath& data_path,
             const base::FilePath& my_files_path,
             const std::string& desired_mount_dir_name,
             base::OnceCallback<void(base::FilePath)> callback) override {
    callback_ = std::move(callback);
    OnMountCalled(token, data_path, desired_mount_dir_name);
  }

  MOCK_METHOD3(OnMountCalled,
               void(const base::UnguessableToken& token,
                    const base::FilePath& data_path,
                    const std::string& desired_mount_dir_name));

  void CompleteMount(const base::FilePath& mount_path) {
    CHECK(callback_);
    std::move(callback_).Run(mount_path);
  }

  static void AssertNotMounted() { ASSERT_EQ(0, gInstanceCounter); }

 private:
  static int gInstanceCounter;
  base::OnceCallback<void(base::FilePath)> callback_;
};

int MockDiskMounter::gInstanceCounter = 0;

class MockDriveFsConnection : public DriveFsConnection,
                              public mojom::DriveFsInterceptorForTesting {
 public:
  MockDriveFsConnection() = default;

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

  ~MockDriveFsConnection() override = default;

  base::UnguessableToken Connect(mojom::DriveFsDelegate* delegate,
                                 base::OnceClosure on_disconnected) override {
    CHECK(!delegate_);
    on_disconnected_ = std::move(on_disconnected);
    delegate_ = delegate;
    OnConnected(delegate);
    return base::UnguessableToken::Create();
  }

  mojom::DriveFs& GetDriveFs() override { return *this; }

  MOCK_METHOD1(OnConnected, void(mojom::DriveFsDelegate* delegate));

  void ForceDisconnect() {
    if (on_disconnected_) {
      std::move(on_disconnected_).Run();
    }
  }

 private:
  mojom::DriveFs* GetForwardingInterface() override {
    NOTREACHED_IN_MIGRATION();
    return nullptr;
  }

  raw_ptr<mojom::DriveFsDelegate, DanglingUntriaged> delegate_ = nullptr;
  base::OnceClosure on_disconnected_;
};

class DriveFsSessionForTest : public DriveFsSession {
 public:
  DriveFsSessionForTest(base::OneShotTimer* timer,
                        std::unique_ptr<DiskMounter> disk_mounter,
                        std::unique_ptr<DriveFsConnection> connection,
                        const base::FilePath& data_path,
                        const base::FilePath& my_files_path,
                        const std::string& desired_mount_dir_name,
                        MountObserver* observer)
      : DriveFsSession(timer,
                       std::move(disk_mounter),
                       std::move(connection),
                       data_path,
                       my_files_path,
                       desired_mount_dir_name,
                       observer) {}

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

  ~DriveFsSessionForTest() override = default;

 private:
  void GetAccessToken(const std::string& client_id,
                      const std::string& app_id,
                      const std::vector<std::string>& scopes,
                      GetAccessTokenCallback callback) override {}
  void GetAccessTokenWithExpiry(
      const std::string& client_id,
      const std::string& app_id,
      const std::vector<std::string>& scopes,
      GetAccessTokenWithExpiryCallback callback) override {}
  void OnSyncingStatusUpdate(mojom::SyncingStatusPtr status) override {}
  void OnItemProgress(mojom::ProgressEventPtr item_progress) override {}
  void OnFilesChanged(std::vector<mojom::FileChangePtr> changes) override {}
  void OnError(mojom::DriveErrorPtr error) override {}
  void OnTeamDrivesListReady(
      const std::vector<std::string>& team_drive_ids) override {}
  void OnTeamDriveChanged(
      const std::string& team_drive_id,
      mojom::DriveFsDelegate::CreateOrDelete change_type) override {}
  void ConnectToExtension(
      mojom::ExtensionConnectionParamsPtr params,
      mojo::PendingReceiver<mojom::NativeMessagingPort> port,
      mojo::PendingRemote<mojom::NativeMessagingHost> host,
      ConnectToExtensionCallback callback) override {}
  void DisplayConfirmDialog(mojom::DialogReasonPtr error,
                            DisplayConfirmDialogCallback callback) override {}
  void ExecuteHttpRequest(
      mojom::HttpRequestPtr request,
      mojo::PendingRemote<mojom::HttpDelegate> delegate) override {}
  void GetMachineRootID(
      mojom::DriveFsDelegate::GetMachineRootIDCallback callback) override {}
  void PersistMachineRootID(const std::string& id) override {}
  void OnMirrorSyncingStatusUpdate(mojom::SyncingStatusPtr status) override {}
  void OnNotificationReceived(
      mojom::DriveFsNotificationPtr notification) override {}
  void OnMirrorSyncError(mojom::MirrorSyncErrorListPtr error_list) override {}
};

class DriveFsSessionTest : public ::testing::Test,
                           public DriveFsSession::MountObserver {
 public:
  DriveFsSessionTest() {}

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

 protected:
  MOCK_METHOD1(OnMounted, void(const base::FilePath& path));
  MOCK_METHOD1(OnUnmounted, void(std::optional<base::TimeDelta> delay));
  MOCK_METHOD2(OnMountFailed,
               void(MountFailure failure,
                    std::optional<base::TimeDelta> delay));

  void StartMounting() {
    DCHECK(!holder_);
    DCHECK(!session_);
    auto mounter = std::make_unique<MockDiskMounter>();
    auto connection = std::make_unique<MockDriveFsConnection>();
    holder_ = std::make_unique<PointerHolder>();
    holder_->mounter = mounter.get();
    holder_->connection = connection.get();

    base::FilePath data_path(kExpectedDataDir);

    EXPECT_CALL(*holder_->connection, OnConnected(_));
    EXPECT_CALL(*holder_->mounter,
                OnMountCalled(_, data_path, kExpectedMountDir));
    session_ = std::make_unique<DriveFsSessionForTest>(
        &timer_, std::move(mounter), std::move(connection), data_path,
        base::FilePath(kExpectedMyFilesDir), kExpectedMountDir, this);
    holder_->delegate = session_.get();
    ASSERT_FALSE(session_->is_mounted());
  }

  void CompleteDiskMount() {
    holder_->mounter->CompleteMount(base::FilePath(kExpectedMountPath));
  }

  void ConfirmDriveFsMounted() { holder_->delegate->OnMounted(); }

  void FinishMounting() {
    CompleteDiskMount();
    ASSERT_FALSE(session_->is_mounted());
    EXPECT_CALL(*this, OnMounted(base::FilePath(kExpectedMountPath)));
    ConfirmDriveFsMounted();
    ASSERT_TRUE(session_->is_mounted());
  }

  void DoUnmount() {
    session_.reset();
    MockDiskMounter::AssertNotMounted();
    holder_.reset();
  }

  base::test::TaskEnvironment task_environment_;

  struct PointerHolder {
    raw_ptr<MockDiskMounter, DanglingUntriaged> mounter = nullptr;
    raw_ptr<MockDriveFsConnection, DanglingUntriaged> connection = nullptr;
    raw_ptr<mojom::DriveFsDelegate, DanglingUntriaged> delegate = nullptr;
  };
  base::MockOneShotTimer timer_;
  std::unique_ptr<PointerHolder> holder_;
  std::unique_ptr<DriveFsSession> session_;
};

}  // namespace

TEST_F(DriveFsSessionTest, OnMounted_DisksThenDriveFs) {
  EXPECT_CALL(*this, OnMountFailed(_, _)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);
  ASSERT_NO_FATAL_FAILURE(StartMounting());

  CompleteDiskMount();
  ASSERT_FALSE(session_->is_mounted());
  EXPECT_CALL(*this, OnMounted(base::FilePath(kExpectedMountPath)));
  ConfirmDriveFsMounted();
  ASSERT_TRUE(session_->is_mounted());

  EXPECT_EQ(base::FilePath(kExpectedMountPath), session_->mount_path());
  ASSERT_NO_FATAL_FAILURE(DoUnmount());
}

TEST_F(DriveFsSessionTest, OnMounted_DriveFsThenDisks) {
  EXPECT_CALL(*this, OnMountFailed(_, _)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);
  ASSERT_NO_FATAL_FAILURE(StartMounting());

  ConfirmDriveFsMounted();
  ASSERT_FALSE(session_->is_mounted());
  EXPECT_CALL(*this, OnMounted(base::FilePath(kExpectedMountPath)));
  CompleteDiskMount();
  ASSERT_TRUE(session_->is_mounted());

  EXPECT_EQ(base::FilePath(kExpectedMountPath), session_->mount_path());
  ASSERT_NO_FATAL_FAILURE(DoUnmount());
}

TEST_F(DriveFsSessionTest, OnMountFailed_InDriveFs) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  CompleteDiskMount();

  EXPECT_CALL(*this, OnMountFailed(MountFailure::kUnknown, kEmptyDelay));
  holder_->delegate->OnMountFailed(kEmptyDelay);
  ASSERT_FALSE(session_->is_mounted());
}

TEST_F(DriveFsSessionTest, OnMountFailed_InDisks) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());

  EXPECT_CALL(*this, OnMountFailed(MountFailure::kInvocation, kEmptyDelay));
  holder_->mounter->CompleteMount({});
  ASSERT_FALSE(session_->is_mounted());
}

TEST_F(DriveFsSessionTest, OnMountFailed_DriveFsNeedsRestart) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  CompleteDiskMount();

  EXPECT_CALL(*this, OnMountFailed(MountFailure::kNeedsRestart, kDefaultDelay));
  holder_->delegate->OnMountFailed(kDefaultDelay);
  ASSERT_FALSE(session_->is_mounted());
}

TEST_F(DriveFsSessionTest, OnMountFailed_UnmountInObserver) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  EXPECT_CALL(*this, OnMountFailed(MountFailure::kInvocation, kEmptyDelay))
      .WillOnce(testing::InvokeWithoutArgs([&]() { session_.reset(); }));
  holder_->mounter->CompleteMount({});
  ASSERT_FALSE(session_);
}

TEST_F(DriveFsSessionTest, DestroyBeforeMojoConnection) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  CompleteDiskMount();

  session_.reset();
}

TEST_F(DriveFsSessionTest, DestroyBeforeMountEvent) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  ConfirmDriveFsMounted();

  session_.reset();
}

TEST_F(DriveFsSessionTest, UnmountByRemote) {
  EXPECT_CALL(*this, OnMountFailed(_, _)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  ASSERT_NO_FATAL_FAILURE(FinishMounting());

  EXPECT_CALL(*this, OnUnmounted(kDefaultDelay));
  holder_->delegate->OnUnmounted(kDefaultDelay);
}

TEST_F(DriveFsSessionTest, BreakConnectionAfterMount) {
  EXPECT_CALL(*this, OnMountFailed(_, _)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  ASSERT_NO_FATAL_FAILURE(FinishMounting());

  EXPECT_CALL(*this, OnUnmounted(kEmptyDelay));
  holder_->connection->ForceDisconnect();
}

TEST_F(DriveFsSessionTest, BreakConnectionBeforeMount) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  CompleteDiskMount();

  EXPECT_CALL(*this, OnMountFailed(MountFailure::kIpcDisconnect, kEmptyDelay));
  holder_->connection->ForceDisconnect();
}

TEST_F(DriveFsSessionTest, BreakConnectionOnUnmount) {
  EXPECT_CALL(*this, OnMountFailed(_, _)).Times(0);
  ASSERT_NO_FATAL_FAILURE(StartMounting());
  ASSERT_NO_FATAL_FAILURE(FinishMounting());

  EXPECT_CALL(*this, OnUnmounted(kDefaultDelay))
      .WillOnce(testing::InvokeWithoutArgs(
          [&]() { holder_->connection->ForceDisconnect(); }));
  holder_->delegate->OnUnmounted(kDefaultDelay);
  ASSERT_NO_FATAL_FAILURE(DoUnmount());
}

TEST_F(DriveFsSessionTest, MountTimeout) {
  EXPECT_CALL(*this, OnMounted(_)).Times(0);
  EXPECT_CALL(*this, OnUnmounted(_)).Times(0);

  ASSERT_NO_FATAL_FAILURE(StartMounting());
  CompleteDiskMount();

  EXPECT_CALL(*this, OnMountFailed(MountFailure::kTimeout, kEmptyDelay));
  timer_.Fire();
}

}  // namespace drivefs