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

// StorageMonitorLinux unit tests.

#include "components/storage_monitor/storage_monitor_linux.h"

#include <mntent.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include <array>
#include <memory>
#include <string>

#include "base/check.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/task_environment.h"
#include "components/storage_monitor/mock_removable_storage_observer.h"
#include "components/storage_monitor/removable_device_constants.h"
#include "components/storage_monitor/storage_info.h"
#include "components/storage_monitor/storage_monitor.h"
#include "components/storage_monitor/test_storage_monitor.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace storage_monitor {

namespace {

const char kValidFS[] = "vfat";
const char kInvalidFS[] = "invalidfs";

const char kInvalidPath[] = "invalid path does not exist";

const char kDeviceDCIM1[] = "d1";
const char kDeviceDCIM2[] = "d2";
const char kDeviceDCIM3[] = "d3";
const char kDeviceNoDCIM[] = "d4";
const char kDeviceFixed[] = "d5";

const char kInvalidDevice[] = "invalid_device";

const char kMountPointA[] = "mnt_a";
const char kMountPointB[] = "mnt_b";
const char kMountPointC[] = "mnt_c";

struct TestDeviceData {
  const char* device_path;
  const char* unique_id;
  StorageInfo::Type type;
  uint64_t partition_size_in_bytes;
};

constexpr auto kTestDeviceData = std::to_array<TestDeviceData>({
    {kDeviceDCIM1, "UUID:FFF0-000F",
     StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM, 88788},
    {kDeviceDCIM2, "VendorModelSerial:ComName:Model2010:8989",
     StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM, 8773},
    {kDeviceDCIM3, "VendorModelSerial:::WEM319X792",
     StorageInfo::REMOVABLE_MASS_STORAGE_WITH_DCIM, 22837},
    {kDeviceNoDCIM, "UUID:ABCD-1234",
     StorageInfo::REMOVABLE_MASS_STORAGE_NO_DCIM, 512},
    {kDeviceFixed, "UUID:743A-2349", StorageInfo::FIXED_MASS_STORAGE, 17282},
});

std::unique_ptr<StorageInfo> GetDeviceInfo(const base::FilePath& device_path,
                                           const base::FilePath& mount_point) {
  bool device_found = false;
  size_t i = 0;
  for (; i < std::size(kTestDeviceData); i++) {
    if (device_path.value() == kTestDeviceData[i].device_path) {
      device_found = true;
      break;
    }
  }
  DCHECK(device_found);

  StorageInfo::Type type = kTestDeviceData[i].type;
  auto storage_info = std::make_unique<StorageInfo>(
      StorageInfo::MakeDeviceId(type, kTestDeviceData[i].unique_id),
      mount_point.value(), u"volume label", u"vendor name", u"model name",
      kTestDeviceData[i].partition_size_in_bytes);
  return storage_info;
}

uint64_t GetDevicePartitionSize(const std::string& device) {
  for (const auto& data : kTestDeviceData) {
    if (device == data.device_path)
      return data.partition_size_in_bytes;
  }
  return 0;
}

std::string GetDeviceId(const std::string& device) {
  for (const auto& data : kTestDeviceData) {
    if (device == data.device_path)
      return StorageInfo::MakeDeviceId(data.type, data.unique_id);
  }
  if (device == kInvalidDevice) {
    return StorageInfo::MakeDeviceId(StorageInfo::FIXED_MASS_STORAGE,
                                     kInvalidDevice);
  }
  return std::string();
}

class TestStorageMonitorLinux : public StorageMonitorLinux {
 public:
  explicit TestStorageMonitorLinux(const base::FilePath& path)
      : StorageMonitorLinux(path) {
    SetGetDeviceInfoCallbackForTest(base::BindRepeating(&GetDeviceInfo));
  }

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

  ~TestStorageMonitorLinux() override = default;

  void SetOnMtabUpdateCallback(base::OnceClosure on_mtab_update_callback) {
    EXPECT_FALSE(on_mtab_update_callback_);
    on_mtab_update_callback_ = std::move(on_mtab_update_callback);
  }

 private:
  void UpdateMtab(
      const MtabWatcherLinux::MountPointDeviceMap& new_mtab) override {
    StorageMonitorLinux::UpdateMtab(new_mtab);
    if (on_mtab_update_callback_)
      std::move(on_mtab_update_callback_).Run();
  }

  base::OnceClosure on_mtab_update_callback_;
};

class StorageMonitorLinuxTest : public testing::Test {
 public:
  struct MtabTestData {
    MtabTestData(const std::string& mount_device,
                 const std::string& mount_point,
                 const std::string& mount_type)
        : mount_device(mount_device),
          mount_point(mount_point),
          mount_type(mount_type) {
    }

    const std::string mount_device;
    const std::string mount_point;
    const std::string mount_type;
  };

  StorageMonitorLinuxTest() = default;

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

  ~StorageMonitorLinuxTest() override = default;

 protected:
  void SetUp() override {
    // Create and set up a temp dir with files for the test.
    ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
    base::FilePath test_dir =
        scoped_temp_dir_.GetPath().AppendASCII("test_etc");
    ASSERT_TRUE(base::CreateDirectory(test_dir));
    mtab_file_ = test_dir.AppendASCII("test_mtab");
    MtabTestData initial_test_data[] = {
      MtabTestData("dummydevice", "dummydir", kInvalidFS),
    };
    WriteToMtab(initial_test_data, /*overwrite=*/true);

    monitor_ = std::make_unique<TestStorageMonitorLinux>(mtab_file_);
    mock_storage_observer_ = std::make_unique<MockRemovableStorageObserver>();
    monitor_->AddObserver(mock_storage_observer_.get());

    monitor_->Init();
    task_environment_.RunUntilIdle();
  }

  void TearDown() override {
    task_environment_.RunUntilIdle();
    monitor_->RemoveObserver(mock_storage_observer_.get());
    task_environment_.RunUntilIdle();

    // Linux storage monitor must be destroyed on the UI thread, so do it here.
    monitor_.reset();
  }

  // Append mtab entries from the |data| span to the mtab
  // file, and run the message loop.
  void AppendToMtabAndRunLoop(base::span<const MtabTestData> data) {
    WriteToMtab(data, /*overwrite=*/false);
    WaitForMtabUpdate();
  }

  // Overwrite the mtab file with mtab entries from the |data| span, and run
  // the message loop.
  void OverwriteMtabAndRunLoop(base::span<const MtabTestData> data) {
    WriteToMtab(data, /*overwrite=*/true);
    WaitForMtabUpdate();
  }

  // Simplied version of OverwriteMtabAndRunLoop() that just deletes all the
  // entries in the mtab file.
  void WriteEmptyMtabAndRunLoop() { OverwriteMtabAndRunLoop(/*data=*/{}); }

  // Create a directory named |dir| relative to the test directory.
  // It has a DCIM directory, so StorageMonitorLinux recognizes it as a media
  // directory.
  base::FilePath CreateMountPointWithDCIMDir(const std::string& dir) {
    return CreateMountPoint(dir, /*with_dcim_dir=*/true);
  }

  // Create a directory named |dir| relative to the test directory.
  // It does not have a DCIM directory, so StorageMonitorLinux does not
  // recognize it as a media directory.
  base::FilePath CreateMountPointWithoutDCIMDir(const std::string& dir) {
    return CreateMountPoint(dir, /*with_dcim_dir=*/false);
  }

  void RemoveDCIMDirFromMountPoint(const std::string& dir) {
    base::FilePath dcim =
        scoped_temp_dir_.GetPath().AppendASCII(dir).Append(kDCIMDirectoryName);
    base::DeleteFile(dcim);
  }

  MockRemovableStorageObserver& observer() {
    return *mock_storage_observer_;
  }

  StorageMonitor* notifier() {
    return monitor_.get();
  }

  uint64_t GetStorageSize(const base::FilePath& path) {
    StorageInfo info;
    if (!notifier()->GetStorageInfoForPath(path, &info))
      return 0;

    return info.total_size_in_bytes();
  }

 private:
  // Invoked after making an mtab update. Blocks (in an active RunLoop) until
  // the mtab changes are detected by the file watcher and side-effects of
  // UpdateMtab() propagate.
  void WaitForMtabUpdate() {
    base::RunLoop run_loop;
    monitor_->SetOnMtabUpdateCallback(run_loop.QuitClosure());
    // Wait until the UpdateMtab() notification comes in from the system
    // (cannot use RunUntilIdle right away as that would racily return early
    // per being idle until the system notification comes in).
    run_loop.Run();
    // UpdateMtab() causes asynchronous work on internal task runners, flush
    // everything to make sure `mock_storage_observer_` gets to observe the
    // change.
    task_environment_.RunUntilIdle();
  }

  // Create a directory named |dir| relative to the test directory.
  // Set |with_dcim_dir| to true if the created directory will have a "DCIM"
  // subdirectory.
  // Returns the full path to the created directory on success, or an empty
  // path on failure.
  base::FilePath CreateMountPoint(const std::string& dir, bool with_dcim_dir) {
    base::FilePath return_path(scoped_temp_dir_.GetPath());
    return_path = return_path.AppendASCII(dir);
    base::FilePath path(return_path);
    if (with_dcim_dir)
      path = path.Append(kDCIMDirectoryName);
    if (!base::CreateDirectory(path))
      return base::FilePath();
    return return_path;
  }

  // Write the test mtab data to |mtab_file_|.
  // |data| is a span of mtab entries.
  // |overwrite| specifies whether to overwrite |mtab_file_|.
  void WriteToMtab(base::span<const MtabTestData> data, bool overwrite) {
    FILE* file = setmntent(mtab_file_.value().c_str(), overwrite ? "w" : "a");
    ASSERT_TRUE(file);

    // Due to the glibc *mntent() interface design, which is out of our
    // control, the mtnent struct has several char* fields, even though
    // addmntent() does not write to them in the calls below. To make the
    // compiler happy while avoiding making additional copies of strings,
    // we just const_cast() the strings' c_str()s.
    // Assuming addmntent() does not write to the char* fields, this is safe.
    // It is unlikely the platforms this test suite runs on will have an
    // addmntent() implementation that does change the char* fields. If that
    // was ever the case, the test suite will start crashing or failing.
    mntent entry;
    static const char kMountOpts[] = "rw";
    entry.mnt_opts = const_cast<char*>(kMountOpts);
    entry.mnt_freq = 0;
    entry.mnt_passno = 0;
    for (size_t i = 0; i < data.size(); ++i) {
      entry.mnt_fsname = const_cast<char*>(data[i].mount_device.c_str());
      entry.mnt_dir = const_cast<char*>(data[i].mount_point.c_str());
      entry.mnt_type = const_cast<char*>(data[i].mount_type.c_str());
      ASSERT_EQ(0, addmntent(file, &entry));
    }
    ASSERT_EQ(1, endmntent(file));
  }

  base::test::TaskEnvironment task_environment_;

  std::unique_ptr<MockRemovableStorageObserver> mock_storage_observer_;

  // Temporary directory for created test data.
  base::ScopedTempDir scoped_temp_dir_;
  // Path to the test mtab file.
  base::FilePath mtab_file_;

  std::unique_ptr<TestStorageMonitorLinux> monitor_;
};

// TODO(crbug.com/40822314): This test is flaky.
// Simple test case where we attach and detach a media device.
TEST_F(StorageMonitorLinuxTest, DISABLED_BasicAttachDetach) {
  base::FilePath test_path = CreateMountPointWithDCIMDir(kMountPointA);
  ASSERT_FALSE(test_path.empty());
  MtabTestData test_data[] = {
    MtabTestData(kDeviceDCIM2, test_path.value(), kValidFS),
    MtabTestData(kDeviceFixed, kInvalidPath, kValidFS),
  };
  // Only |kDeviceDCIM2| should be attached, since |kDeviceFixed| has a bad
  // path.
  AppendToMtabAndRunLoop(test_data);

  EXPECT_EQ(1, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());
  EXPECT_EQ(GetDeviceId(kDeviceDCIM2), observer().last_attached().device_id());
  EXPECT_EQ(test_path.value(), observer().last_attached().location());

  // |kDeviceDCIM2| should be detached here.
  WriteEmptyMtabAndRunLoop();
  EXPECT_EQ(1, observer().attach_calls());
  EXPECT_EQ(1, observer().detach_calls());
  EXPECT_EQ(GetDeviceId(kDeviceDCIM2), observer().last_detached().device_id());
}

// Only removable devices are recognized.
// This test is flaky, see https://crbug.com/1012211
TEST_F(StorageMonitorLinuxTest, Removable) {
  base::FilePath test_path_a = CreateMountPointWithDCIMDir(kMountPointA);
  ASSERT_FALSE(test_path_a.empty());
  MtabTestData test_data1[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
  };
  // |kDeviceDCIM1| should be attached as expected.
  AppendToMtabAndRunLoop(test_data1);

  EXPECT_EQ(1, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());
  EXPECT_EQ(GetDeviceId(kDeviceDCIM1), observer().last_attached().device_id());
  EXPECT_EQ(test_path_a.value(), observer().last_attached().location());

  // This should do nothing, since |kDeviceFixed| is not removable.
  base::FilePath test_path_b = CreateMountPointWithoutDCIMDir(kMountPointB);
  ASSERT_FALSE(test_path_b.empty());
  MtabTestData test_data2[] = {
    MtabTestData(kDeviceFixed, test_path_b.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data2);
  EXPECT_EQ(1, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());

  // |kDeviceDCIM1| should be detached as expected.
  WriteEmptyMtabAndRunLoop();
  EXPECT_EQ(1, observer().attach_calls());
  EXPECT_EQ(1, observer().detach_calls());
  EXPECT_EQ(GetDeviceId(kDeviceDCIM1), observer().last_detached().device_id());

  // |kDeviceNoDCIM| should be attached as expected.
  MtabTestData test_data3[] = {
    MtabTestData(kDeviceNoDCIM, test_path_b.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data3);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(1, observer().detach_calls());
  EXPECT_EQ(GetDeviceId(kDeviceNoDCIM), observer().last_attached().device_id());
  EXPECT_EQ(test_path_b.value(), observer().last_attached().location());

  // |kDeviceNoDCIM| should be detached as expected.
  WriteEmptyMtabAndRunLoop();
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(2, observer().detach_calls());
  EXPECT_EQ(GetDeviceId(kDeviceNoDCIM), observer().last_detached().device_id());
}

// More complicated test case with multiple devices on multiple mount points.
TEST_F(StorageMonitorLinuxTest, SwapMountPoints) {
  base::FilePath test_path_a = CreateMountPointWithDCIMDir(kMountPointA);
  base::FilePath test_path_b = CreateMountPointWithDCIMDir(kMountPointB);
  ASSERT_FALSE(test_path_a.empty());
  ASSERT_FALSE(test_path_b.empty());

  // Attach two devices.
  // (*'d mounts are those StorageMonitor knows about.)
  // kDeviceDCIM1 -> kMountPointA *
  // kDeviceDCIM2 -> kMountPointB *
  MtabTestData test_data1[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
    MtabTestData(kDeviceDCIM2, test_path_b.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data1);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());

  // Detach two devices from old mount points and attach the devices at new
  // mount points.
  // kDeviceDCIM1 -> kMountPointB *
  // kDeviceDCIM2 -> kMountPointA *
  MtabTestData test_data2[] = {
    MtabTestData(kDeviceDCIM1, test_path_b.value(), kValidFS),
    MtabTestData(kDeviceDCIM2, test_path_a.value(), kValidFS),
  };
  OverwriteMtabAndRunLoop(test_data2);
  EXPECT_EQ(4, observer().attach_calls());
  EXPECT_EQ(2, observer().detach_calls());

  // Detach all devices.
  WriteEmptyMtabAndRunLoop();
  EXPECT_EQ(4, observer().attach_calls());
  EXPECT_EQ(4, observer().detach_calls());
}

// More complicated test case with multiple devices on multiple mount points.
TEST_F(StorageMonitorLinuxTest, MultiDevicesMultiMountPoints) {
  base::FilePath test_path_a = CreateMountPointWithDCIMDir(kMountPointA);
  base::FilePath test_path_b = CreateMountPointWithDCIMDir(kMountPointB);
  ASSERT_FALSE(test_path_a.empty());
  ASSERT_FALSE(test_path_b.empty());

  // Attach two devices.
  // (*'d mounts are those StorageMonitor knows about.)
  // kDeviceDCIM1 -> kMountPointA *
  // kDeviceDCIM2 -> kMountPointB *
  MtabTestData test_data1[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
    MtabTestData(kDeviceDCIM2, test_path_b.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data1);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());

  // Attach |kDeviceDCIM1| to |kMountPointB|.
  // |kDeviceDCIM2| is inaccessible, so it is detached. |kDeviceDCIM1| has been
  // attached at |kMountPointB|, but is still accessible from |kMountPointA|.
  // kDeviceDCIM1 -> kMountPointA *
  // kDeviceDCIM2 -> kMountPointB
  // kDeviceDCIM1 -> kMountPointB
  MtabTestData test_data2[] = {
    MtabTestData(kDeviceDCIM1, test_path_b.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data2);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(1, observer().detach_calls());

  // Detach |kDeviceDCIM1| from |kMountPointA|, causing a detach and attach
  // event.
  // kDeviceDCIM2 -> kMountPointB
  // kDeviceDCIM1 -> kMountPointB *
  MtabTestData test_data3[] = {
    MtabTestData(kDeviceDCIM2, test_path_b.value(), kValidFS),
    MtabTestData(kDeviceDCIM1, test_path_b.value(), kValidFS),
  };
  OverwriteMtabAndRunLoop(test_data3);
  EXPECT_EQ(3, observer().attach_calls());
  EXPECT_EQ(2, observer().detach_calls());

  // Attach |kDeviceDCIM1| to |kMountPointA|.
  // kDeviceDCIM2 -> kMountPointB
  // kDeviceDCIM1 -> kMountPointB *
  // kDeviceDCIM1 -> kMountPointA
  MtabTestData test_data4[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data4);
  EXPECT_EQ(3, observer().attach_calls());
  EXPECT_EQ(2, observer().detach_calls());

  // Detach |kDeviceDCIM1| from |kMountPointB|.
  // kDeviceDCIM1 -> kMountPointA *
  // kDeviceDCIM2 -> kMountPointB *
  OverwriteMtabAndRunLoop(test_data1);
  EXPECT_EQ(5, observer().attach_calls());
  EXPECT_EQ(3, observer().detach_calls());

  // Detach all devices.
  WriteEmptyMtabAndRunLoop();
  EXPECT_EQ(5, observer().attach_calls());
  EXPECT_EQ(5, observer().detach_calls());
}

TEST_F(StorageMonitorLinuxTest, MultipleMountPointsWithNonDCIMDevices) {
  base::FilePath test_path_a = CreateMountPointWithDCIMDir(kMountPointA);
  base::FilePath test_path_b = CreateMountPointWithDCIMDir(kMountPointB);
  ASSERT_FALSE(test_path_a.empty());
  ASSERT_FALSE(test_path_b.empty());

  // Attach to one first.
  // (*'d mounts are those StorageMonitor knows about.)
  // kDeviceDCIM1 -> kMountPointA *
  MtabTestData test_data1[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data1);
  EXPECT_EQ(1, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());

  // Attach |kDeviceDCIM1| to |kMountPointB|.
  // kDeviceDCIM1 -> kMountPointA *
  // kDeviceDCIM1 -> kMountPointB
  MtabTestData test_data2[] = {
    MtabTestData(kDeviceDCIM1, test_path_b.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data2);
  EXPECT_EQ(1, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());

  // Attach |kDeviceFixed| (a non-removable device) to |kMountPointA|.
  // kDeviceDCIM1 -> kMountPointA
  // kDeviceDCIM1 -> kMountPointB *
  // kDeviceFixed -> kMountPointA
  MtabTestData test_data3[] = {
    MtabTestData(kDeviceFixed, test_path_a.value(), kValidFS),
  };
  RemoveDCIMDirFromMountPoint(kMountPointA);
  AppendToMtabAndRunLoop(test_data3);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(1, observer().detach_calls());

  // Detach |kDeviceFixed|.
  // kDeviceDCIM1 -> kMountPointA
  // kDeviceDCIM1 -> kMountPointB *
  MtabTestData test_data4[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
    MtabTestData(kDeviceDCIM1, test_path_b.value(), kValidFS),
  };
  CreateMountPointWithDCIMDir(kMountPointA);
  OverwriteMtabAndRunLoop(test_data4);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(1, observer().detach_calls());

  // Attach |kDeviceNoDCIM| (a non-DCIM device) to |kMountPointB|.
  // kDeviceDCIM1  -> kMountPointA *
  // kDeviceDCIM1  -> kMountPointB
  // kDeviceNoDCIM -> kMountPointB *
  MtabTestData test_data5[] = {
    MtabTestData(kDeviceNoDCIM, test_path_b.value(), kValidFS),
  };
  base::DeleteFile(test_path_b.Append(kDCIMDirectoryName));
  AppendToMtabAndRunLoop(test_data5);
  EXPECT_EQ(4, observer().attach_calls());
  EXPECT_EQ(2, observer().detach_calls());

  // Detach |kDeviceNoDCIM|.
  // kDeviceDCIM1 -> kMountPointA *
  // kDeviceDCIM1 -> kMountPointB
  MtabTestData test_data6[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
    MtabTestData(kDeviceDCIM1, test_path_b.value(), kValidFS),
  };
  CreateMountPointWithDCIMDir(kMountPointB);
  OverwriteMtabAndRunLoop(test_data6);
  EXPECT_EQ(4, observer().attach_calls());
  EXPECT_EQ(3, observer().detach_calls());

  // Detach |kDeviceDCIM1| from |kMountPointB|.
  // kDeviceDCIM1 -> kMountPointA *
  OverwriteMtabAndRunLoop(test_data1);
  EXPECT_EQ(4, observer().attach_calls());
  EXPECT_EQ(3, observer().detach_calls());

  // Detach all devices.
  WriteEmptyMtabAndRunLoop();
  EXPECT_EQ(4, observer().attach_calls());
  EXPECT_EQ(4, observer().detach_calls());
}

TEST_F(StorageMonitorLinuxTest, DeviceLookUp) {
  base::FilePath test_path_a = CreateMountPointWithDCIMDir(kMountPointA);
  base::FilePath test_path_b = CreateMountPointWithoutDCIMDir(kMountPointB);
  base::FilePath test_path_c = CreateMountPointWithoutDCIMDir(kMountPointC);
  ASSERT_FALSE(test_path_a.empty());
  ASSERT_FALSE(test_path_b.empty());
  ASSERT_FALSE(test_path_c.empty());

  // Attach to one first.
  // (starred mounts are those StorageMonitor knows about.)
  // kDeviceDCIM1  -> kMountPointA *
  // kDeviceNoDCIM -> kMountPointB *
  // kDeviceFixed  -> kMountPointC
  MtabTestData test_data1[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
    MtabTestData(kDeviceNoDCIM, test_path_b.value(), kValidFS),
    MtabTestData(kDeviceFixed, test_path_c.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data1);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());

  StorageInfo device_info;
  EXPECT_TRUE(notifier()->GetStorageInfoForPath(test_path_a, &device_info));
  EXPECT_EQ(GetDeviceId(kDeviceDCIM1), device_info.device_id());
  EXPECT_EQ(test_path_a.value(), device_info.location());
  EXPECT_EQ(88788ULL, device_info.total_size_in_bytes());
  EXPECT_EQ(u"volume label", device_info.storage_label());
  EXPECT_EQ(u"vendor name", device_info.vendor_name());
  EXPECT_EQ(u"model name", device_info.model_name());

  EXPECT_TRUE(notifier()->GetStorageInfoForPath(test_path_b, &device_info));
  EXPECT_EQ(GetDeviceId(kDeviceNoDCIM), device_info.device_id());
  EXPECT_EQ(test_path_b.value(), device_info.location());

  EXPECT_TRUE(notifier()->GetStorageInfoForPath(test_path_c, &device_info));
  EXPECT_EQ(GetDeviceId(kDeviceFixed), device_info.device_id());
  EXPECT_EQ(test_path_c.value(), device_info.location());

  // An invalid path.
  EXPECT_FALSE(notifier()->GetStorageInfoForPath(base::FilePath(kInvalidPath),
                                                 &device_info));

  // Test filling in of the mount point.
  EXPECT_TRUE(
      notifier()->GetStorageInfoForPath(test_path_a.Append("some/other/path"),
      &device_info));
  EXPECT_EQ(GetDeviceId(kDeviceDCIM1), device_info.device_id());
  EXPECT_EQ(test_path_a.value(), device_info.location());

  // One device attached at multiple points.
  // kDeviceDCIM1 -> kMountPointA *
  // kDeviceFixed -> kMountPointB
  // kDeviceFixed -> kMountPointC
  MtabTestData test_data2[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
    MtabTestData(kDeviceFixed, test_path_b.value(), kValidFS),
    MtabTestData(kDeviceFixed, test_path_c.value(), kValidFS),
  };
  AppendToMtabAndRunLoop(test_data2);

  EXPECT_TRUE(notifier()->GetStorageInfoForPath(test_path_a, &device_info));
  EXPECT_EQ(GetDeviceId(kDeviceDCIM1), device_info.device_id());

  EXPECT_TRUE(notifier()->GetStorageInfoForPath(test_path_b, &device_info));
  EXPECT_EQ(GetDeviceId(kDeviceFixed), device_info.device_id());

  EXPECT_TRUE(notifier()->GetStorageInfoForPath(test_path_c, &device_info));
  EXPECT_EQ(GetDeviceId(kDeviceFixed), device_info.device_id());

  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(1, observer().detach_calls());
}

TEST_F(StorageMonitorLinuxTest, DevicePartitionSize) {
  base::FilePath test_path_a = CreateMountPointWithDCIMDir(kMountPointA);
  base::FilePath test_path_b = CreateMountPointWithoutDCIMDir(kMountPointB);
  ASSERT_FALSE(test_path_a.empty());
  ASSERT_FALSE(test_path_b.empty());

  MtabTestData test_data1[] = {
    MtabTestData(kDeviceDCIM1, test_path_a.value(), kValidFS),
    MtabTestData(kDeviceNoDCIM, test_path_b.value(), kValidFS),
    MtabTestData(kDeviceFixed, kInvalidPath, kInvalidFS),
  };
  AppendToMtabAndRunLoop(test_data1);
  EXPECT_EQ(2, observer().attach_calls());
  EXPECT_EQ(0, observer().detach_calls());

  EXPECT_EQ(GetDevicePartitionSize(kDeviceDCIM1),
            GetStorageSize(test_path_a));
  EXPECT_EQ(GetDevicePartitionSize(kDeviceNoDCIM),
            GetStorageSize(test_path_b));
  EXPECT_EQ(GetDevicePartitionSize(kInvalidPath),
            GetStorageSize(base::FilePath(kInvalidPath)));
}

}  // namespace

}  // namespace storage_monitor
