chromium/chrome/browser/ui/webui/print_preview/print_preview_handler_chromeos_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/ui/webui/print_preview/print_preview_handler_chromeos.h"

#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "chrome/browser/ash/crosapi/test_crosapi_dependency_registry.h"
#include "chrome/browser/printing/print_test_utils.h"
#include "chrome/browser/ui/webui/print_preview/print_preview_handler.h"
#include "chrome/browser/ui/webui/print_preview/print_preview_ui.h"
#include "chrome/browser/ui/webui/print_preview/print_preview_utils.h"
#include "chrome/browser/ui/webui/print_preview/printer_handler.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/chromeos/printing/fake_local_printer_chromeos.h"
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_web_ui.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/crosapi/idle_service_ash.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#endif

namespace printing {

namespace {

std::vector<crosapi::mojom::LocalDestinationInfoPtr>
ConvertToLocalDestinationInfo(std::vector<std::string> printer_ids) {
  std::vector<crosapi::mojom::LocalDestinationInfoPtr> local_printers;
  for (const auto& printer_id : printer_ids) {
    crosapi::mojom::LocalDestinationInfoPtr local_printer =
        crosapi::mojom::LocalDestinationInfo::New();
    local_printer->id = printer_id;
    local_printers.push_back(std::move(local_printer));
  }
  return local_printers;
}

}  // namespace

const char kSelectedPrintServerId[] = "selected-print-server-id";
const char kSelectedPrintServerName[] = "Print Server Name";

class TestLocalPrinter : public FakeLocalPrinter {
 public:
  TestLocalPrinter() = default;
  TestLocalPrinter(const TestLocalPrinter&) = delete;
  TestLocalPrinter& operator=(const TestLocalPrinter&) = delete;
  ~TestLocalPrinter() override { EXPECT_FALSE(print_server_ids_); }

  std::vector<std::string> TakePrintServerIds() {
    std::vector<std::string> print_server_ids = std::move(*print_server_ids_);
    print_server_ids_.reset();
    return print_server_ids;
  }

  // crosapi::mojom::LocalPrinter:
  void ChoosePrintServers(const std::vector<std::string>& print_server_ids,
                          ChoosePrintServersCallback callback) override {
    EXPECT_FALSE(print_server_ids_);
    print_server_ids_ = print_server_ids;
  }
  void AddPrintServerObserver(
      mojo::PendingRemote<crosapi::mojom::PrintServerObserver> remote,
      AddPrintServerObserverCallback callback) override {
    EXPECT_FALSE(remote_);
    EXPECT_TRUE(remote);
    remote_ =
        mojo::Remote<crosapi::mojom::PrintServerObserver>(std::move(remote));
    std::move(callback).Run();
  }
  void GetPrintServersConfig(GetPrintServersConfigCallback callback) override {
    ASSERT_TRUE(config_);
    std::move(callback).Run(std::move(config_));
    config_ = nullptr;
  }
  void AddLocalPrintersObserver(
      mojo::PendingRemote<crosapi::mojom::LocalPrintersObserver> remote,
      AddLocalPrintersObserverCallback callback) override {
    std::move(callback).Run(std::move(local_printers_));
  }

  void SetLocalPrinters(std::vector<std::string> printer_ids) {
    local_printers_ = ConvertToLocalDestinationInfo(printer_ids);
  }

 private:
  friend class PrintPreviewHandlerChromeOSTest;

  std::vector<crosapi::mojom::LocalDestinationInfoPtr> local_printers_;
  mojo::Remote<crosapi::mojom::PrintServerObserver> remote_;
  std::optional<std::vector<std::string>> print_server_ids_;
  crosapi::mojom::PrintServersConfigPtr config_;
};

class FakePrintPreviewUI : public PrintPreviewUI {
 public:
  FakePrintPreviewUI(content::WebUI* web_ui,
                     std::unique_ptr<PrintPreviewHandler> handler)
      : PrintPreviewUI(web_ui, std::move(handler)) {}
  FakePrintPreviewUI(const FakePrintPreviewUI&) = delete;
  FakePrintPreviewUI& operator=(const FakePrintPreviewUI&) = delete;
  ~FakePrintPreviewUI() override = default;

 private:
};

struct PrinterInfo {
  std::string id;
  bool is_default;
  base::Value::Dict basic_info;
  base::Value::Dict capabilities;
};

class TestPrinterHandlerChromeOS : public PrinterHandler {
 public:
  explicit TestPrinterHandlerChromeOS(
      const std::vector<PrinterInfo>& printers) {
    SetPrinters(printers);
  }
  TestPrinterHandlerChromeOS(const TestPrinterHandlerChromeOS&) = delete;
  TestPrinterHandlerChromeOS& operator=(const TestPrinterHandlerChromeOS&) =
      delete;
  ~TestPrinterHandlerChromeOS() override = default;

  void Reset() override {}

  void GetDefaultPrinter(DefaultPrinterCallback cb) override {
    std::move(cb).Run(default_printer_);
  }

  void StartGetPrinters(AddedPrintersCallback added_printers_callback,
                        GetPrintersDoneCallback done_callback) override {
    if (!printers_.empty()) {
      added_printers_callback.Run(printers_.Clone());
    }
    std::move(done_callback).Run();
  }

  void StartGetCapability(const std::string& destination_id,
                          GetCapabilityCallback callback) override {
    std::move(callback).Run(printer_capabilities_[destination_id].Clone());
  }

  void StartGrantPrinterAccess(const std::string& printer_id,
                               GetPrinterInfoCallback callback) override {}

  void StartPrint(const std::u16string& job_title,
                  base::Value::Dict settings,
                  scoped_refptr<base::RefCountedMemory> print_data,
                  PrintCallback callback) override {
    std::move(callback).Run(base::Value());
  }

  void SetPrinters(const std::vector<PrinterInfo>& printers) {
    printers_.clear();
    for (const auto& printer : printers) {
      if (printer.is_default) {
        default_printer_ = printer.id;
      }
      printers_.Append(printer.basic_info.Clone());
      printer_capabilities_[printer.id] = printer.capabilities.Clone();
    }
  }

 private:
  std::string default_printer_;
  base::Value::List printers_;
  std::map<std::string, base::Value::Dict> printer_capabilities_;
};

class TestPrintPreviewHandlerChromeOS : public PrintPreviewHandlerChromeOS {
 public:
  explicit TestPrintPreviewHandlerChromeOS(
      std::unique_ptr<PrinterHandler> printer_handler)
      : test_printer_handler_(std::move(printer_handler)) {}

  PrinterHandler* GetPrinterHandler(mojom::PrinterType printer_type) override {
    return test_printer_handler_.get();
  }

 private:
  std::unique_ptr<PrinterHandler> test_printer_handler_;
};

PrinterInfo GetSimplePrinterInfo(const std::string& name, bool is_default) {
  PrinterInfo simple_printer;
  simple_printer.id = name;
  simple_printer.is_default = is_default;
  simple_printer.basic_info.Set("printer_name", simple_printer.id);
  simple_printer.basic_info.Set("printer_description", "Printer for test");
  simple_printer.basic_info.Set("printer_status", 1);
  base::Value::Dict cdd;
  simple_printer.capabilities.Set("printer", simple_printer.basic_info.Clone());
  simple_printer.capabilities.Set("capabilities", cdd.Clone());
  return simple_printer;
}

class PrintPreviewHandlerChromeOSTest : public testing::Test {
 public:
  PrintPreviewHandlerChromeOSTest() = default;
  PrintPreviewHandlerChromeOSTest(const PrintPreviewHandlerChromeOSTest&) =
      delete;
  PrintPreviewHandlerChromeOSTest& operator=(
      const PrintPreviewHandlerChromeOSTest&) = delete;
  ~PrintPreviewHandlerChromeOSTest() override = default;

  void SetUp() override {
#if BUILDFLAG(IS_CHROMEOS_ASH)
    ASSERT_TRUE(testing_profile_manager_.SetUp());
    crosapi::IdleServiceAsh::DisableForTesting();
    ash::LoginState::Initialize();
    manager_ = crosapi::CreateCrosapiManagerWithTestRegistry();
#endif
    preview_web_contents_ = content::WebContents::Create(
        content::WebContents::CreateParams(&profile_));
    web_ui_ = std::make_unique<content::TestWebUI>();
    web_ui_->set_web_contents(preview_web_contents_.get());

    // Create printer handler.
    printers_.push_back(
        GetSimplePrinterInfo(test::kPrinterName, /*is_default=*/true));
    auto printer_handler =
        std::make_unique<TestPrinterHandlerChromeOS>(printers_);
    printer_handler_ = printer_handler.get();

    auto preview_handler = std::make_unique<TestPrintPreviewHandlerChromeOS>(
        std::move(printer_handler));
    preview_handler->SetInitiatorForTesting(preview_web_contents_.get());
    handler_ = preview_handler.get();
    local_printer_ = std::make_unique<TestLocalPrinter>();
    handler_->local_printer_ = local_printer_.get();
    web_ui()->AddMessageHandler(std::move(preview_handler));
    handler_->AllowJavascriptForTesting();

    auto preview_ui = std::make_unique<FakePrintPreviewUI>(
        web_ui(), std::make_unique<PrintPreviewHandler>());
    web_ui()->SetController(std::move(preview_ui));
  }

  void TearDown() override {
#if BUILDFLAG(IS_CHROMEOS_ASH)
    manager_.reset();
    ash::LoginState::Shutdown();
#endif
  }

  void DisableAshChrome() {
    local_printer_ = nullptr;
    handler_->local_printer_ = nullptr;
  }

  void AssertWebUIEventFired(const content::TestWebUI::CallData& data,
                             const std::string& event_id) {
    EXPECT_EQ("cr.webUIListenerCallback", data.function_name());
    ASSERT_TRUE(data.arg1()->is_string());
    EXPECT_EQ(event_id, data.arg1()->GetString());
  }

  content::TestWebUI* web_ui() { return web_ui_.get(); }
  void ChangePrintServersConfig(crosapi::mojom::PrintServersConfigPtr config) {
    EXPECT_TRUE(local_printer_->remote_);
    local_printer_->config_ = config.Clone();
    // Call the callback directly instead of through the mojo remote
    // so that it is synchronous.
    handler_->OnPrintServersChanged(std::move(config));
  }
  std::vector<std::string> TakePrintServerIds() {
    return local_printer_->TakePrintServerIds();
  }
  void ChangeServerPrinters() { handler_->OnServerPrintersChanged(); }
  TestPrinterHandlerChromeOS* printer_handler() { return printer_handler_; }
  std::vector<PrinterInfo>& printers() { return printers_; }

  void SetLocalPrinters(std::vector<std::string> printer_ids) {
    local_printer_->SetLocalPrinters(printer_ids);
  }

  void FireOnLocalPrintersUpdated(std::vector<std::string> printer_ids) {
    handler_->OnLocalPrintersUpdated(
        ConvertToLocalDestinationInfo(printer_ids));
  }

#if BUILDFLAG(IS_CHROMEOS_LACROS)
  int LocalPrinterVersion() {
    return handler_->GetLocalPrinterVersionForTesting();
  }
#endif

 private:
  content::BrowserTaskEnvironment task_environment_;
#if BUILDFLAG(IS_CHROMEOS_ASH)
  TestingProfileManager testing_profile_manager_{
      TestingBrowserProcess::GetGlobal()};
  std::unique_ptr<crosapi::CrosapiManager> manager_;
#endif
  TestingProfile profile_;
  std::unique_ptr<TestLocalPrinter> local_printer_;
  std::unique_ptr<content::WebContents> preview_web_contents_;
  std::unique_ptr<content::TestWebUI> web_ui_;
  raw_ptr<PrintPreviewHandlerChromeOS> handler_;
  raw_ptr<TestPrinterHandlerChromeOS> printer_handler_;
  std::vector<PrinterInfo> printers_;
};

TEST_F(PrintPreviewHandlerChromeOSTest, ChoosePrintServersNoAsh) {
  DisableAshChrome();

  base::Value::List selected_args;
  base::Value::List selected_ids_js;
  selected_ids_js.Append(kSelectedPrintServerId);
  selected_args.Append(std::move(selected_ids_js));

  web_ui()->HandleReceivedMessage("choosePrintServers", selected_args);
  AssertWebUIEventFired(*web_ui()->call_data().back(),
                        "server-printers-loading");
  EXPECT_EQ(web_ui()->call_data().back()->arg2()->GetBool(), true);
}

TEST_F(PrintPreviewHandlerChromeOSTest, GetPrintServersConfigNoAsh) {
  DisableAshChrome();
  base::Value::List args;
  args.Append("callback_id");
  web_ui()->HandleReceivedMessage("getPrintServersConfig", args);
  EXPECT_EQ("cr.webUIResponse", web_ui()->call_data().back()->function_name());
  EXPECT_EQ(base::Value("callback_id"), *web_ui()->call_data().back()->arg1());
  EXPECT_EQ(base::Value(true), *web_ui()->call_data().back()->arg2());
  EXPECT_EQ(base::Value(), *web_ui()->call_data().back()->arg3());
}

TEST_F(PrintPreviewHandlerChromeOSTest, ChoosePrintServers) {
  base::Value::List selected_args;
  base::Value::List selected_ids_js;
  selected_ids_js.Append(kSelectedPrintServerId);
  selected_args.Append(std::move(selected_ids_js));

  base::Value::List none_selected_args;
  base::Value::List none_selected_js;
  none_selected_args.Append(std::move(none_selected_js));

  web_ui()->HandleReceivedMessage("choosePrintServers", selected_args);
  EXPECT_THAT(TakePrintServerIds(),
              testing::ElementsAre(std::string(kSelectedPrintServerId)));
  web_ui()->HandleReceivedMessage("choosePrintServers", none_selected_args);
  EXPECT_THAT(TakePrintServerIds(), testing::IsEmpty());
  AssertWebUIEventFired(*web_ui()->call_data().back(),
                        "server-printers-loading");
  EXPECT_EQ(web_ui()->call_data().back()->arg2()->GetBool(), true);
}

TEST_F(PrintPreviewHandlerChromeOSTest, OnPrintServersChanged) {
  std::vector<crosapi::mojom::PrintServerPtr> servers;
  servers.push_back(crosapi::mojom::PrintServer::New(
      kSelectedPrintServerId, GURL("http://print-server.com"),
      kSelectedPrintServerName));

  crosapi::mojom::PrintServersConfigPtr config =
      crosapi::mojom::PrintServersConfig::New();
  config->print_servers = std::move(servers);
  config->fetching_mode = ash::ServerPrintersFetchingMode::kStandard;
  ChangePrintServersConfig(std::move(config));
  auto* call_data = web_ui()->call_data().back().get();
  AssertWebUIEventFired(*call_data, "print-servers-config-changed");
  const base::Value::List* printer_list =
      call_data->arg2()->GetDict().FindList("printServers");
  bool is_single_server_fetching_mode =
      call_data->arg2()
          ->GetDict()
          .FindBool("isSingleServerFetchingMode")
          .value();

  ASSERT_EQ(printer_list->size(), 1u);
  const base::Value::Dict& first_printer = printer_list->front().GetDict();
  EXPECT_EQ(*first_printer.FindString("id"), kSelectedPrintServerId);
  EXPECT_EQ(*first_printer.FindString("name"), kSelectedPrintServerName);
  EXPECT_EQ(is_single_server_fetching_mode, false);

  base::Value::List args;
  args.Append("callback_id");
  web_ui()->HandleReceivedMessage("getPrintServersConfig", args);
  const base::Value kExpectedConfig = base::test::ParseJson(R"({
    "isSingleServerFetchingMode": false,
    "printServers": [ {
      "id": "selected-print-server-id",
      "name": "Print Server Name" } ]
  })");
  EXPECT_EQ("cr.webUIResponse", web_ui()->call_data().back()->function_name());
  EXPECT_EQ(base::Value("callback_id"), *web_ui()->call_data().back()->arg1());
  EXPECT_EQ(base::Value(true), *web_ui()->call_data().back()->arg2());
  EXPECT_EQ(kExpectedConfig, *web_ui()->call_data().back()->arg3());
}

TEST_F(PrintPreviewHandlerChromeOSTest, OnServerPrintersUpdated) {
  ChangeServerPrinters();
  AssertWebUIEventFired(*web_ui()->call_data().back(),
                        "server-printers-loading");
  EXPECT_EQ(web_ui()->call_data().back()->arg2()->GetBool(), false);
}

TEST_F(PrintPreviewHandlerChromeOSTest, HandlePrinterSetup) {
  base::Value::Dict media_1;
  media_1.Set("width_microns", 100);
  media_1.Set("height_microns", 200);
  base::Value::Dict media_2;
  media_2.Set("width_microns", 300);
  media_2.Set("is_continuous_feed", true);
  // After filtering, the expected media will just have the discrete media.
  base::Value::List expected_media;
  expected_media.Append(media_1.Clone());

  base::Value::List option_list;
  option_list.Append(std::move(media_1));
  option_list.Append(std::move(media_2));
  base::Value::Dict media_size;
  media_size.Set("option", std::move(option_list));
  base::Value::Dict printer;
  printer.Set("media_size", std::move(media_size));
  base::Value::Dict cdd;
  cdd.Set("printer", std::move(printer));

  ASSERT_EQ(1u, printers().size());
  printers()[0].capabilities.Set(kSettingCapabilities, std::move(cdd));
  printer_handler()->SetPrinters(printers());

  base::Value::List args;
  args.Append("callback_id");
  args.Append(test::kPrinterName);
  web_ui()->HandleReceivedMessage("setupPrinter", args);

  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  EXPECT_EQ("cr.webUIResponse", data.function_name());
  ASSERT_TRUE(data.arg1()->is_string());
  EXPECT_EQ("callback_id", data.arg1()->GetString());
  ASSERT_TRUE(data.arg2()->is_bool());
  EXPECT_TRUE(data.arg2()->GetBool());
  ASSERT_TRUE(data.arg3()->is_dict());
  const base::Value::Dict* cdd_result =
      data.arg3()->GetDict().FindDict(kSettingCapabilities);
  ASSERT_TRUE(cdd_result);
  const base::Value::List* options = GetMediaSizeOptionsFromCdd(*cdd_result);
  ASSERT_TRUE(options);
  EXPECT_EQ(expected_media, *options);
}

// Verify 'getShowManagePrinters' can be called.
TEST_F(PrintPreviewHandlerChromeOSTest, HandleGetCanShowManagePrinters) {
  const std::string callback_id = "callback-id";
  base::Value::List args;
  args.Append(callback_id);
  web_ui()->HandleReceivedMessage("getShowManagePrinters", args);

  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  EXPECT_EQ("cr.webUIResponse", data.function_name());
  ASSERT_TRUE(data.arg1()->is_string());
  EXPECT_EQ(callback_id, data.arg1()->GetString());
  ASSERT_TRUE(data.arg2()->is_bool());
  ASSERT_TRUE(data.arg2()->GetBool());
}

// Verify 'observeLocalPrinters' can be called.
TEST_F(PrintPreviewHandlerChromeOSTest, HandleObserveLocalPrinters) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  if (int{crosapi::mojom::LocalPrinter::MethodMinVersions::
              kAddLocalPrintersObserverMinVersion} > LocalPrinterVersion()) {
    LOG(ERROR) << "Local printer version incompatible";
    return;
  }
#endif

  const std::vector<std::string> printers{"Printer1", "Printer2", "Printer3"};
  SetLocalPrinters(printers);

  const std::string callback_id = "callback-id";
  base::Value::List args;
  args.Append(callback_id);
  web_ui()->HandleReceivedMessage("observeLocalPrinters", args);

  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  EXPECT_EQ("cr.webUIResponse", data.function_name());
  ASSERT_TRUE(data.arg1()->is_string());
  EXPECT_EQ(callback_id, data.arg1()->GetString());
  // True if ResolveJavascriptCallback and false if RejectJavascriptCallback
  // is called by the handler.
  EXPECT_TRUE(data.arg2()->GetBool());
  EXPECT_EQ(printers.size(), data.arg3()->GetList().size());
}

// Verify 'local-printers-updated' is fired when the observer is triggered.
TEST_F(PrintPreviewHandlerChromeOSTest, FireLocalPrintersUpdated) {
  const std::vector<std::string> printers{"Printer1", "Printer2", "Printer3"};
  FireOnLocalPrintersUpdated(printers);

  const content::TestWebUI::CallData& data = *web_ui()->call_data().back();
  AssertWebUIEventFired(data, "local-printers-updated");
  EXPECT_EQ(printers.size(), data.arg2()->GetList().size());
}

}  // namespace printing