chromium/chrome/browser/ui/webui/ash/settings/pages/files/google_drive_handler_browsertest.cc

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

#include <initializer_list>

#include "ash/constants/ash_features.h"
#include "base/files/file_util.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ash/drive/drive_integration_service_browser_test_base.h"
#include "chrome/browser/ash/drive/file_system_util.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/webui/ash/settings/pages/files/mojom/google_drive_handler.mojom.h"
#include "chrome/browser/ui/webui/ash/settings/test_support/os_settings_browser_test_mixin.h"
#include "chrome/test/data/webui/chromeos/settings/test_api.test-mojom-test-utils.h"
#include "chromeos/ash/components/dbus/spaced/fake_spaced_client.h"
#include "chromeos/ash/components/drivefs/fake_drivefs.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom-forward.h"
#include "content/public/test/browser_test.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "ui/base/text/bytes_formatting.h"

using base::test::RunOnceCallback;
using testing::_;
using testing::DoAll;
using testing::InSequence;
using testing::Return;
using testing::TestParamInfo;
using testing::ValuesIn;
using testing::WithParamInterface;

namespace ash::settings {
namespace {

const std::string FormatBytesToString(int64_t bytes) {
  return base::UTF16ToUTF8(ui::FormatBytes(bytes));
}

// Provides a minimal interface to initialize a drive item with only the
// required fields.
struct DriveItem {
  int64_t size;
  bool available_offline;
  bool pinned;
};

struct TestParam {
  std::string test_suffix;
  std::vector<base::test::FeatureRef> enabled_features;
  std::vector<base::test::FeatureRef> disabled_features;
};

std::string ParamToTestSuffix(const TestParamInfo<TestParam>& info) {
  return info.param.test_suffix;
}

class FakeSearchQuery : public drivefs::mojom::SearchQuery {
 public:
  void SetSearchResults(std::vector<DriveItem> page_items) {
    std::vector<drivefs::mojom::QueryItemPtr> result;
    result.reserve(page_items.size());
    for (const auto& item : page_items) {
      drivefs::mojom::QueryItemPtr p = drivefs::mojom::QueryItem::New();
      // Paths must be parented at "/root" to be considered for space
      // calculations.
      p->path =
          base::FilePath("/root").Append(base::NumberToString(++item_counter_));
      p->metadata = drivefs::mojom::FileMetadata::New();
      p->metadata->capabilities = drivefs::mojom::Capabilities::New();
      p->metadata->size = item.size;
      p->metadata->available_offline = item.available_offline;
      p->metadata->pinned = item.pinned;
      p->metadata->stable_id = item_counter_;
      result.push_back(std::move(p));
    }
    pages_.push_back(std::move(result));
  }

  void GetNextPage(GetNextPageCallback callback) override {
    ASSERT_LT(page_counter_, pages_.size()) << "Another page was expected";
    std::optional<std::vector<drivefs::mojom::QueryItemPtr>> items(
        std::move(pages_.at(page_counter_++)));
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), drive::FILE_ERROR_OK,
                                  std::move(items)));
  }

 private:
  size_t page_counter_ = 0;
  size_t item_counter_ = 0;
  std::vector<std::vector<drivefs::mojom::QueryItemPtr>> pages_;
};

class GoogleDriveHandlerBaseTest
    : public drive::DriveIntegrationServiceBrowserTestBase {
 public:
  GoogleDriveHandlerBaseTest() : receiver_(&fake_search_query_) {}

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

  void SetUp() override {
    drive::DriveIntegrationServiceBrowserTestBase::SetUp();
    ash::SpacedClient::InitializeFake();
  }

  // Open Google drive settings and returns the `GoogleDriveSettings` page
  // object to interact with the test api.
  mojom::GoogleDriveSettingsAsyncWaiter OpenGoogleDriveSettings() {
    os_settings_driver_remote_ =
        mojo::Remote(os_settings_mixin_.OpenOSSettings("/googleDrive"));
    auto os_settings_driver =
        mojom::OSSettingsDriverAsyncWaiter(os_settings_driver_remote_.get());
    google_drive_settings_remote_ =
        mojo::Remote(os_settings_driver.AssertOnGoogleDriveSettings());
    return mojom::GoogleDriveSettingsAsyncWaiter(
        google_drive_settings_remote_.get());
  }

  void SetUpSearchResultExpectations() {
    // Ensure when a search query is made, it binds to the mock search query.
    auto* fake_drivefs = GetFakeDriveFsForProfile(browser()->profile());
    EXPECT_CALL(*fake_drivefs, StartSearchQuery(_, _))
        .WillOnce([&](mojo::PendingReceiver<drivefs::mojom::SearchQuery>
                          pending_receiver,
                      drivefs::mojom::QueryParametersPtr query_params) {
          receiver_.Bind(std::move(pending_receiver));
        });
  }

  base::FilePath CreateFileInContentCache(int file_size_in_bytes) {
    auto* const service =
        drive::util::GetIntegrationServiceByProfile(browser()->profile());
    {
      // Ensure the content cache directory exists.
      base::ScopedAllowBlockingForTesting allow_blocking;
      if (!base::DirectoryExists(service->GetDriveFsContentCachePath())) {
        EXPECT_TRUE(
            base::CreateDirectory(service->GetDriveFsContentCachePath()));
      }
    }
    const base::FilePath file_path =
        service->GetDriveFsContentCachePath().Append("foo.txt");
    {
      // Create a file of `file_size_in_bytes` bytes in the content_cache
      // directory.
      base::ScopedAllowBlockingForTesting allow_blocking;
      EXPECT_TRUE(base::WriteFile(file_path,
                                  base::RandBytesAsString(file_size_in_bytes)));
    }

    return file_path;
  }

 protected:
  OSSettingsBrowserTestMixin os_settings_mixin_{&mixin_host_};
  FakeSearchQuery fake_search_query_;
  base::test::ScopedFeatureList scoped_feature_list_;

 private:
  mojo::Remote<mojom::OSSettingsDriver> os_settings_driver_remote_;
  mojo::Remote<mojom::GoogleDriveSettings> google_drive_settings_remote_;
  mojo::Receiver<drivefs::mojom::SearchQuery> receiver_;
};

class GoogleDriveHandlerBulkPinningTest : public GoogleDriveHandlerBaseTest {
 public:
  GoogleDriveHandlerBulkPinningTest() {
    scoped_feature_list_.InitWithFeatures(
        {ash::features::kDriveFsBulkPinning,
         ash::features::kFeatureManagementDriveFsBulkPinning},
        {});
  }
};

IN_PROC_BROWSER_TEST_F(GoogleDriveHandlerBulkPinningTest,
                       NoSearchResultsReturnsNoRequiredOnlyFreeSpace) {
  SetUpSearchResultExpectations();
  fake_search_query_.SetSearchResults({});

  // Expect the free space to be 1 GB (1,073,741,824 bytes), the required space
  // to be 0 KB (0 items).
  int64_t free_space = 1024 * 1024 * 1024;
  auto required_space = FormatBytesToString(0);
  auto remaining_space = FormatBytesToString(free_space);

  // Fake the free space returned, open Google drive settings and ensure the
  // values on the google drive subpage element match those returned via the
  // observable.
  ash::FakeSpacedClient::Get()->set_free_disk_space(free_space);
  auto google_drive_settings = OpenGoogleDriveSettings();
  google_drive_settings.AssertBulkPinningSpace(required_space, remaining_space);
}

IN_PROC_BROWSER_TEST_F(GoogleDriveHandlerBulkPinningTest,
                       OnlyUnpinnedResultsUpdateTheSpaceRequirements) {
  SetUpSearchResultExpectations();

  // Each item is 125 MB in size, total required space should be 500 MB.
  int64_t file_size = 125 * 1024 * 1024;
  fake_search_query_.SetSearchResults(
      {{.size = file_size}, {.size = file_size}});
  fake_search_query_.SetSearchResults(
      {{.size = file_size}, {.size = file_size}});
  fake_search_query_.SetSearchResults({});

  int64_t free_space = int64_t(3) << 30;  // 3 GB.
  auto required_space = FormatBytesToString(file_size * 4);
  auto free_space_str = FormatBytesToString(free_space);

  ash::FakeSpacedClient::Get()->set_free_disk_space(free_space);
  auto google_drive_settings = OpenGoogleDriveSettings();
  google_drive_settings.AssertBulkPinningSpace(required_space, free_space_str);
}

class GoogleDriveHandlerTest : public GoogleDriveHandlerBaseTest,
                               public WithParamInterface<TestParam> {
 public:
  GoogleDriveHandlerTest() {
    scoped_feature_list_.InitWithFeatures(GetParam().enabled_features,
                                          GetParam().disabled_features);
  }
};

IN_PROC_BROWSER_TEST_P(GoogleDriveHandlerTest,
                       TotalPinnedSizeUpdatesValueOnElement) {
  // Mock no search results are returned (this avoids the call to
  // `CalculateRequiredSpace` from being ran here).
  fake_search_query_.SetSearchResults({});
  ash::FakeSpacedClient::Get()->set_free_disk_space(int64_t(3) << 30);

  CreateFileInContentCache(32);

  auto google_drive_settings = OpenGoogleDriveSettings();
  google_drive_settings.AssertContentCacheSize(FormatBytesToString(4096));
}

IN_PROC_BROWSER_TEST_P(GoogleDriveHandlerTest,
                       ClearingOfflineFilesCallsProperMethods) {
  // Mock no search results are returned (this avoids the call to
  // `CalculateRequiredSpace` from being ran here).
  fake_search_query_.SetSearchResults({});
  ash::FakeSpacedClient::Get()->set_free_disk_space(int64_t(3) << 30);

  const base::FilePath file_path = CreateFileInContentCache(32);

  auto* fake_drivefs = GetFakeDriveFsForProfile(browser()->profile());
  EXPECT_CALL(*fake_drivefs, ClearOfflineFiles(_))
      .WillOnce(
          [&file_path](
              drivefs::mojom::DriveFs::ClearOfflineFilesCallback callback) {
            {
              base::ScopedAllowBlockingForTesting allow_blocking;
              ASSERT_TRUE(base::DeleteFile(file_path));
            }
            std::move(callback).Run(drive::FILE_ERROR_OK);
          });

  auto google_drive_settings = OpenGoogleDriveSettings();
  google_drive_settings.ClickClearOfflineFilesAndAssertNewSize(
      FormatBytesToString(0));
}

const TestParam kTestParams[] = {
    {
        .test_suffix = "BulkPinning",
        .enabled_features =
            {ash::features::kDriveFsBulkPinning,
             ash::features::kFeatureManagementDriveFsBulkPinning},
        .disabled_features = {},
    },

    // OsSettingsRevampWayfinding feature test variations
    {
        .test_suffix = "BulkPinning_Revamp",
        .enabled_features =
            {ash::features::kOsSettingsRevampWayfinding,
             ash::features::kDriveFsBulkPinning,
             ash::features::kFeatureManagementDriveFsBulkPinning},
        .disabled_features = {},
    },
};

INSTANTIATE_TEST_SUITE_P(,
                         GoogleDriveHandlerTest,
                         ValuesIn(kTestParams),
                         &ParamToTestSuffix);

}  // namespace
}  // namespace ash::settings