chromium/chrome/browser/ui/webui/ash/settings/pages/printing/cups_printers_handler_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 "chrome/browser/ui/webui/ash/settings/pages/printing/cups_printers_handler.h"

#include <memory>
#include <string>

#include "ash/public/cpp/test/test_new_window_delegate.h"
#include "base/containers/flat_set.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_string_value_serializer.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "chrome/browser/ash/printing/fake_cups_printers_manager.h"
#include "chrome/browser/download/chrome_download_manager_delegate.h"
#include "chrome/browser/download/download_core_service_factory.h"
#include "chrome/browser/download/download_core_service_impl.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/ui/chrome_select_file_policy.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/dbus/printscanmgr/fake_printscanmgr_client.h"
#include "chromeos/ash/components/dbus/printscanmgr/printscanmgr_client.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_web_ui.h"
#include "printing/backend/test_print_backend.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/shell_dialogs/select_file_dialog.h"
#include "ui/shell_dialogs/select_file_dialog_factory.h"
#include "url/gurl.h"

namespace ash::settings {

namespace {

constexpr char kSavedPrintersCountHistogramName[] =
    "Printing.CUPS.SavedPrintersCount";

constexpr char kHandlerFunctionName[] = "handlerFunctionName";

void AddPrinterToPrintScanManager(const std::string& printer_id,
                                  const std::string& ppd) {
  printscanmgr::CupsAddManuallyConfiguredPrinterRequest request;
  request.set_name(printer_id);
  request.set_ppd_contents(ppd);
  base::RunLoop run_loop;
  PrintscanmgrClient::Get()->CupsAddManuallyConfiguredPrinter(
      std::move(request),
      base::IgnoreArgs<std::optional<
          printscanmgr::CupsAddManuallyConfiguredPrinterResponse>>(
          run_loop.QuitClosure()));
  run_loop.Run();
}

}  // namespace

using ::chromeos::Printer;

class CupsPrintersHandlerTest;

// Callback used for testing CupsAddAutoConfiguredPrinter().
void AddedPrinter(int32_t status) {
  ASSERT_EQ(status, 0);
}

// Callback used for testing CupsRemovePrinter().
void RemovedPrinter(base::OnceClosure quit_closure,
                    bool* expected,
                    bool result) {
  *expected = result;
  std::move(quit_closure).Run();
}

class FakePpdProvider : public chromeos::PpdProvider {
 public:
  FakePpdProvider() = default;

  void ResolveManufacturers(ResolveManufacturersCallback cb) override {}
  void ResolvePrinters(const std::string& manufacturer,
                       ResolvePrintersCallback cb) override {}
  void ResolvePpdReference(const chromeos::PrinterSearchData& search_data,
                           ResolvePpdReferenceCallback cb) override {}
  void ResolvePpd(const Printer::PpdReference& reference,
                  ResolvePpdCallback cb) override {}
  void ResolvePpdLicense(std::string_view effective_make_and_model,
                         ResolvePpdLicenseCallback cb) override {}
  void ReverseLookup(const std::string& effective_make_and_model,
                     ReverseLookupCallback cb) override {}

 private:
  ~FakePpdProvider() override {}
};

// A fake ui::SelectFileDialog, which will cancel the file selection instead of
// selecting a file and verify that the extensions are correctly set.
class FakeSelectFileDialog : public ui::SelectFileDialog {
 public:
  FakeSelectFileDialog(Listener* listener,
                       std::unique_ptr<ui::SelectFilePolicy> policy,
                       FileTypeInfo* file_type)
      : ui::SelectFileDialog(listener, std::move(policy)),
        expected_file_type_info_(file_type) {}

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

 protected:
  void SelectFileImpl(Type type,
                      const std::u16string& title,
                      const base::FilePath& default_path,
                      const FileTypeInfo* file_types,
                      int file_type_index,
                      const base::FilePath::StringType& default_extension,
                      gfx::NativeWindow owning_window,
                      const GURL* caller) override {
    // Check that the extensions we expect match the actual extensions passed
    // from the CupsPrintersHandler.
    VerifyExtensions(file_types);
    // Close the file select dialog.
    listener_->FileSelectionCanceled();
  }

  bool IsRunning(gfx::NativeWindow owning_window) const override {
    return true;
  }
  void ListenerDestroyed() override { listener_ = nullptr; }
  bool HasMultipleFileTypeChoicesImpl() override { return false; }

  void VerifyExtensions(const FileTypeInfo* file_types) {
    const std::vector<std::vector<base::FilePath::StringType>>& actual_exts =
        file_types->extensions;
    std::vector<std::vector<base::FilePath::StringType>> expected_exts =
        expected_file_type_info_->extensions;

    for (std::vector<base::FilePath::StringType> actual : actual_exts) {
      bool is_equal = false;
      std::sort(actual.begin(), actual.end());
      for (auto expected_it = expected_exts.begin();
           expected_it != expected_exts.end(); ++expected_it) {
        std::vector<base::FilePath::StringType>& expected = *expected_it;
        std::sort(expected.begin(), expected.end());
        if (expected == actual) {
          is_equal = true;
          expected_exts.erase(expected_it);
          break;
        }
      }
      ASSERT_TRUE(is_equal);
    }
  }

 private:
  ~FakeSelectFileDialog() override = default;

  raw_ptr<ui::SelectFileDialog::FileTypeInfo> expected_file_type_info_;
};

// A factory associated with the artificial file picker.
class TestSelectFileDialogFactory : public ui::SelectFileDialogFactory {
 public:
  explicit TestSelectFileDialogFactory(
      ui::SelectFileDialog::FileTypeInfo* expected_file_type_info)
      : expected_file_type_info_(expected_file_type_info) {}

  ui::SelectFileDialog* Create(
      ui::SelectFileDialog::Listener* listener,
      std::unique_ptr<ui::SelectFilePolicy> policy) override {
    // TODO(jimmyxgong): Investigate why using |policy| created by
    // CupsPrintersHandler crashes the test.
    return new FakeSelectFileDialog(listener, nullptr,
                                    expected_file_type_info_);
  }

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

 private:
  raw_ptr<ui::SelectFileDialog::FileTypeInfo> expected_file_type_info_;
};

class MockNewWindowDelegate : public testing::NiceMock<TestNewWindowDelegate> {
 public:
  // TestNewWindowDelegate:
  MOCK_METHOD(void,
              OpenUrl,
              (const GURL& url, OpenUrlFrom from, Disposition disposition),
              (override));
};

class CupsPrintersHandlerTest : public testing::Test {
 public:
  constexpr static const std::string kPpdPrinterName = "printer_name";
  CupsPrintersHandlerTest()
      : task_environment_(content::BrowserTaskEnvironment::REAL_IO_THREAD),
        profile_(std::make_unique<TestingProfile>()),
        web_ui_(),
        printers_handler_() {}
  ~CupsPrintersHandlerTest() override = default;

  void SetUp() override {
    printers_handler_ = CupsPrintersHandler::CreateForTesting(
        profile_.get(), base::MakeRefCounted<FakePpdProvider>(),
        &printers_manager_);
    printers_handler_->SetWebUIForTest(&web_ui_);
    printers_handler_->RegisterMessages();
    printers_handler_->AllowJavascriptForTesting();
    printing::PrintBackend::SetPrintBackendForTesting(print_backend_.get());
    PrintscanmgrClient::InitializeFake();
    // Initialize NewWindowDelegate things.
    auto instance = std::make_unique<MockNewWindowDelegate>();
    auto primary = std::make_unique<MockNewWindowDelegate>();
    new_window_delegate_primary_ = primary.get();
    new_window_provider_ = std::make_unique<TestNewWindowDelegateProvider>(
        std::move(instance), std::move(primary));

    DownloadCoreServiceFactory::GetForBrowserContext(profile_.get())
        ->SetDownloadManagerDelegateForTesting(
            std::make_unique<ChromeDownloadManagerDelegate>(profile_.get()));
    // Use a temporary directory for downloads.
    ASSERT_TRUE(download_dir_.CreateUniqueTempDir());
    DownloadPrefs* prefs =
        DownloadPrefs::FromDownloadManager(profile_->GetDownloadManager());
    prefs->SetDownloadPath(download_dir_.GetPath());
    prefs->SkipSanitizeDownloadTargetPathForTesting();
  }

  void TearDown() override {
    new_window_provider_.reset();
    PrintscanmgrClient::Shutdown();
    printing::PrintBackend::SetPrintBackendForTesting(nullptr);
  }

  void CallRetrieveCupsPpd(const std::string& printer_id,
                           const std::string& license_url = "",
                           const std::string& printer_name = kPpdPrinterName) {
    base::Value::List args;
    args.Append(printer_id);
    args.Append(printer_name);
    args.Append(license_url);

    web_ui_.HandleReceivedMessage("retrieveCupsPrinterPpd", args);
    run_loop_.Run();
  }

  void CallGetCupsSavedPrintersList() {
    base::Value::List args;
    args.Append(kHandlerFunctionName);
    web_ui_.HandleReceivedMessage("getCupsSavedPrintersList", args);
  }

  // Get the contents of the file that was downloaded.  Return true on success,
  // false on error.
  bool GetDownloadedPpdContents(
      std::string& contents,
      const std::string& printer_name = kPpdPrinterName) const {
    const base::FilePath downloads_path =
        DownloadPrefs::FromDownloadManager(profile_->GetDownloadManager())
            ->DownloadPath();
    const base::FilePath filepath =
        downloads_path.Append(printer_name).AddExtension("ppd");
    return base::ReadFileToString(filepath, &contents);
  }

 protected:
  // Must outlive |profile_|.
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfile> profile_;
  content::TestWebUI web_ui_;
  FakeCupsPrintersManager printers_manager_;
  std::unique_ptr<CupsPrintersHandler> printers_handler_;
  base::RunLoop run_loop_;
  scoped_refptr<printing::TestPrintBackend> print_backend_ =
      base::MakeRefCounted<printing::TestPrintBackend>();
  raw_ptr<MockNewWindowDelegate, DanglingUntriaged>
      new_window_delegate_primary_;
  std::unique_ptr<TestNewWindowDelegateProvider> new_window_provider_;
  base::ScopedTempDir download_dir_;
  base::HistogramTester histogram_tester_;

  const std::string kDefaultPpdData = "PPD data used for testing";
  const std::string kPpdDataStrWithHeader = R"(*PPD-Adobe: "4.3")";
  const std::string kPpdErrorString =
      base::StringPrintf("Unable to retrieve PPD for %s.",
                         kPpdPrinterName.c_str());
};

TEST_F(CupsPrintersHandlerTest, RemoveCorrectPrinter) {
  ConciergeClient::InitializeFake(
      /*fake_cicerone_client=*/nullptr);

  Printer printer("id");
  printers_manager_.SavePrinter(printer);
  printers_manager_.SetUpPrinter(printer, /*is_automatic_installation=*/true,
                                 base::DoNothing());

  const std::string remove_list = R"(
    [")" + printer.id() + R"(", "Test Printer 1"]
  )";
  std::string error;
  base::Value remove_printers = base::test::ParseJson(remove_list);
  ASSERT_TRUE(remove_printers.is_list());

  EXPECT_TRUE(printers_manager_.IsPrinterInstalled(printer));
  web_ui_.HandleReceivedMessage("removeCupsPrinter", remove_printers.GetList());
  EXPECT_FALSE(printers_manager_.IsPrinterInstalled(printer));

  profile_.reset();
  ConciergeClient::Shutdown();
}

TEST_F(CupsPrintersHandlerTest, VerifyOnlyPpdFilesAllowed) {
  ui::SelectFileDialog::FileTypeInfo expected_file_type_info;
  // We only allow .ppd and .ppd.gz file extensions for our file select dialog.
  expected_file_type_info.extensions.push_back({"ppd"});
  expected_file_type_info.extensions.push_back({"ppd.gz"});
  ui::SelectFileDialog::SetFactory(
      std::make_unique<TestSelectFileDialogFactory>(&expected_file_type_info));

  base::Value::List args;
  args.Append("handleFunctionName");
  web_ui_.HandleReceivedMessage("selectPPDFile", args);
}

TEST_F(CupsPrintersHandlerTest, ViewPPD) {
  // Test the nominal case where everything works and the PPD gets downloaded.

  AddPrinterToPrintScanManager("id", kDefaultPpdData);

  Printer printer("id");
  printers_manager_.SavePrinter(printer);

  print_backend_->AddValidPrinter(
      printer.id(),
      std::make_unique<printing::PrinterSemanticCapsAndDefaults>(), nullptr);

  EXPECT_CALL(*new_window_delegate_primary_,
              OpenUrl(testing::Property(&GURL::ExtractFileName,
                                        testing::StartsWith(kPpdPrinterName)),
                      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                      ash::NewWindowDelegate::Disposition::kSwitchToTab))
      .WillOnce(testing::InvokeWithoutArgs(&run_loop_, &base::RunLoop::Quit));

  CallRetrieveCupsPpd(printer.id());

  // Check for the downloaded PPD file.
  std::string contents;
  EXPECT_TRUE(GetDownloadedPpdContents(contents));
  EXPECT_EQ(contents, kDefaultPpdData);
}

TEST_F(CupsPrintersHandlerTest, ViewPPDWithLicense) {
  // Test the nominal case where everything works and the PPD (with a license)
  // gets returned.

  AddPrinterToPrintScanManager("id", kPpdDataStrWithHeader);

  Printer printer("id");
  printers_manager_.SavePrinter(printer);

  print_backend_->AddValidPrinter(
      printer.id(),
      std::make_unique<printing::PrinterSemanticCapsAndDefaults>(), nullptr);

  EXPECT_CALL(*new_window_delegate_primary_,
              OpenUrl(testing::Property(&GURL::ExtractFileName,
                                        testing::StartsWith(kPpdPrinterName)),
                      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                      ash::NewWindowDelegate::Disposition::kSwitchToTab))
      .WillOnce(testing::InvokeWithoutArgs(&run_loop_, &base::RunLoop::Quit));

  const std::string license_url("chrome://os-credits/xerox-printing-license");
  CallRetrieveCupsPpd(printer.id(), license_url);

  // Check that the downloaded PPD file contains the license URL.
  std::string contents;
  EXPECT_TRUE(GetDownloadedPpdContents(contents));
  EXPECT_THAT(contents, testing::HasSubstr(license_url));
  EXPECT_THAT(contents, testing::HasSubstr(kPpdDataStrWithHeader));
}

TEST_F(CupsPrintersHandlerTest, ViewPPDUnsanitizedFilename) {
  // Test the nominal case where the printer has a name that needs sanitized.
  const std::string printer_name("bad/name");
  const std::string sanitized_name("bad_name");

  AddPrinterToPrintScanManager("id", kDefaultPpdData);

  Printer printer("id");
  printers_manager_.SavePrinter(printer);

  print_backend_->AddValidPrinter(
      printer.id(),
      std::make_unique<printing::PrinterSemanticCapsAndDefaults>(), nullptr);

  EXPECT_CALL(*new_window_delegate_primary_,
              OpenUrl(testing::Property(&GURL::ExtractFileName,
                                        testing::StartsWith(sanitized_name)),
                      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                      ash::NewWindowDelegate::Disposition::kSwitchToTab))
      .WillOnce(testing::InvokeWithoutArgs(&run_loop_, &base::RunLoop::Quit));

  CallRetrieveCupsPpd(printer.id(), /*license_url=*/"", printer_name);

  // Check for the downloaded PPD file.
  std::string contents;
  EXPECT_TRUE(GetDownloadedPpdContents(contents, sanitized_name));
  EXPECT_EQ(contents, kDefaultPpdData);
}

TEST_F(CupsPrintersHandlerTest, ViewPPDWithLicenseBadPpd) {
  // Try to view a PPD that contains a license, but the PPD doesn't start with
  // the expected PPD string, so the license can't be inserted, and the PPD
  // can't be downloaded.

  AddPrinterToPrintScanManager("id", kDefaultPpdData);

  Printer printer("id");
  printers_manager_.SavePrinter(printer);

  print_backend_->AddValidPrinter(
      printer.id(),
      std::make_unique<printing::PrinterSemanticCapsAndDefaults>(), nullptr);

  EXPECT_CALL(*new_window_delegate_primary_,
              OpenUrl(testing::Property(&GURL::ExtractFileName,
                                        testing::StartsWith(kPpdPrinterName)),
                      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                      ash::NewWindowDelegate::Disposition::kSwitchToTab))
      .WillOnce(testing::InvokeWithoutArgs(&run_loop_, &base::RunLoop::Quit));

  const std::string license_url("chrome://os-credits/xerox-printing-license");
  CallRetrieveCupsPpd(printer.id(), license_url);

  // Check that the downloaded PPD file contains the error message.
  std::string contents;
  EXPECT_TRUE(GetDownloadedPpdContents(contents));
  EXPECT_THAT(contents, testing::HasSubstr(kPpdErrorString));
}

TEST_F(CupsPrintersHandlerTest, ViewPPDPrinterNotFound) {
  // Test the case where the printer is not known to the printer manager.
  // No printers were added to CupsPrintersManager.

  EXPECT_CALL(*new_window_delegate_primary_,
              OpenUrl(testing::Property(&GURL::ExtractFileName,
                                        testing::StartsWith(kPpdPrinterName)),
                      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                      ash::NewWindowDelegate::Disposition::kSwitchToTab))
      .WillOnce(testing::InvokeWithoutArgs(&run_loop_, &base::RunLoop::Quit));

  CallRetrieveCupsPpd("printer_id");

  // Check that the downloaded PPD file contains the error message.
  std::string contents;
  EXPECT_TRUE(GetDownloadedPpdContents(contents));
  EXPECT_THAT(contents, testing::HasSubstr(kPpdErrorString));
}

TEST_F(CupsPrintersHandlerTest, ViewPPDPrinterNotSetup) {
  // Test the case where the printer is known but not setup.

  AddPrinterToPrintScanManager("id", kDefaultPpdData);

  Printer printer("id");
  printers_manager_.SavePrinter(printer);

  print_backend_->AddValidPrinter(
      printer.id(),
      std::make_unique<printing::PrinterSemanticCapsAndDefaults>(), nullptr);

  EXPECT_CALL(*new_window_delegate_primary_,
              OpenUrl(testing::Property(&GURL::ExtractFileName,
                                        testing::StartsWith(kPpdPrinterName)),
                      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                      ash::NewWindowDelegate::Disposition::kSwitchToTab))
      .WillOnce(testing::InvokeWithoutArgs(&run_loop_, &base::RunLoop::Quit));

  CallRetrieveCupsPpd(printer.id());

  // Check for the downloaded PPD file.
  std::string contents;
  EXPECT_TRUE(GetDownloadedPpdContents(contents));
  EXPECT_EQ(contents, kDefaultPpdData);
}

TEST_F(CupsPrintersHandlerTest, ViewPPDEmptyPPD) {
  // Test the case where an empty PPD is returned from printscanmgr.

  AddPrinterToPrintScanManager("id", "");

  Printer printer("id");
  printers_manager_.SavePrinter(printer);

  print_backend_->AddValidPrinter(
      printer.id(),
      std::make_unique<printing::PrinterSemanticCapsAndDefaults>(), nullptr);

  EXPECT_CALL(*new_window_delegate_primary_,
              OpenUrl(testing::Property(&GURL::ExtractFileName,
                                        testing::StartsWith(kPpdPrinterName)),
                      ash::NewWindowDelegate::OpenUrlFrom::kUserInteraction,
                      ash::NewWindowDelegate::Disposition::kSwitchToTab))
      .WillOnce(testing::InvokeWithoutArgs(&run_loop_, &base::RunLoop::Quit));

  CallRetrieveCupsPpd(printer.id());

  // Check that the downloaded PPD file contains the error message.
  std::string contents;
  EXPECT_TRUE(GetDownloadedPpdContents(contents));
  EXPECT_THAT(contents, testing::HasSubstr(kPpdErrorString));
}

TEST_F(CupsPrintersHandlerTest, GetSavedPrinters) {
  Printer printer("id");
  printer.SetUri("http://printer/uri");
  printers_manager_.SavePrinter(printer);
  Printer printer2("id2");
  printer2.SetUri("http://printer/uri2");
  printers_manager_.SavePrinter(printer2);

  CallGetCupsSavedPrintersList();

  // Expect 2 printers are recorded to the histogram from the `GetPrinters()`
  // result.
  histogram_tester_.ExpectBucketCount(kSavedPrintersCountHistogramName,
                                      /*sample=*/2,
                                      /*expected_count=*/1);
}

// Verify the saved printers are sent to the "local-printers-updated" event when
// the LocalPrintersObserver is triggered.
TEST_F(CupsPrintersHandlerTest, LocalPrintersObserver) {
  Printer printer1("id1");
  Printer printer2("id2");
  printers_manager_.SavePrinter(printer1);
  printers_manager_.SavePrinter(printer2);
  printers_manager_.TriggerLocalPrintersObserver();
  const content::TestWebUI::CallData& data = *web_ui_.call_data().back();
  ASSERT_TRUE(data.arg1()->is_string());
  EXPECT_EQ("local-printers-updated", data.arg1()->GetString());
  ASSERT_TRUE(data.arg2()->is_list());
  EXPECT_EQ(2u, data.arg2()->GetList().size());
}

}  // namespace ash::settings