chromium/chrome/browser/extensions/api/printing/printing_api_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/extensions/api/printing/printing_api_handler.h"

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

#include "base/containers/span.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/chromeos/printing/test_cups_wrapper.h"
#include "chrome/browser/extensions/api/printing/fake_print_job_controller.h"
#include "chrome/browser/extensions/api/printing/print_job_submitter.h"
#include "chrome/browser/printing/print_preview_sticky_settings.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/extensions/api/printing.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chrome/test/chromeos/printing/fake_local_printer_chromeos.h"
#include "chromeos/crosapi/mojom/local_printer.mojom.h"
#include "chromeos/printing/printer_configuration.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "content/public/browser/blob_handle.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/event_router_factory.h"
#include "extensions/browser/test_event_router.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_id.h"
#include "printing/backend/print_backend.h"
#include "printing/backend/test_print_backend.h"
#include "printing/mojom/print.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace extensions {

namespace {

class PrintingEventObserver : public TestEventRouter::EventObserver {
 public:
  // The observer will only listen to events with the `event_name`.
  PrintingEventObserver(TestEventRouter* event_router,
                        const std::string& event_name)
      : event_router_(event_router), event_name_(event_name) {
    event_router_->AddEventObserver(this);
  }

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

  void CheckJobStatusEvent(const std::string& expected_extension_id,
                           const std::string& expected_job_id,
                           api::printing::JobStatus expected_status) const {
    EXPECT_EQ(expected_extension_id, extension_id_);
    ASSERT_TRUE(event_args_.is_list());
    ASSERT_EQ(2u, event_args_.GetList().size());
    const base::Value& job_id = event_args_.GetList()[0];
    ASSERT_TRUE(job_id.is_string());
    EXPECT_EQ(expected_job_id, job_id.GetString());
    const base::Value& job_status = event_args_.GetList()[1];
    ASSERT_TRUE(job_status.is_string());
    EXPECT_EQ(expected_status,
              api::printing::ParseJobStatus(job_status.GetString()));
  }

  ~PrintingEventObserver() override {
    event_router_->RemoveEventObserver(this);
  }

  // TestEventRouter::EventObserver:
  void OnDispatchEventToExtension(const std::string& extension_id,
                                  const Event& event) override {
    if (event.event_name == event_name_) {
      extension_id_ = extension_id;
      event_args_ = base::Value(event.event_args.Clone());
    }
  }

  const ExtensionId& extension_id() const { return extension_id_; }

  const base::Value& event_args() const { return event_args_; }

 private:
  // Event router this class should observe.
  const raw_ptr<TestEventRouter> event_router_;

  // The name of the observed event.
  const std::string event_name_;

  // The extension id passed for the last observed event.
  ExtensionId extension_id_;

  // The arguments passed for the last observed event.
  base::Value event_args_;
};

constexpr char kExtensionId[] = "abcdefghijklmnopqrstuvwxyzabcdef";
constexpr char kExtensionId2[] = "abcdefghijklmnopqrstuvwxyzaaaaaa";
constexpr char kPrinterId[] = "printer";

constexpr char kId1[] = "id1";
constexpr char kName[] = "name";
constexpr char kDescription[] = "description";
constexpr char kUri[] = "ipp://1.2.3.4";

constexpr int kHorizontalDpi = 300;
constexpr int kVerticalDpi = 400;
constexpr int kMediaSizeWidth = 210000;
constexpr int kMediaSizeHeight = 297000;
constexpr char kMediaSizeVendorId[] = "iso_a4_210x297mm";

// CJT stands for Cloud Job Ticket. It should be passed as a print settings
// ticket to chrome.printing.submitJob() method.
constexpr char kCjt[] = R"(
    {
      "version": "1.0",
      "print": {
        "color": {
          "type": "STANDARD_COLOR"
        },
        "duplex": {
          "type": "NO_DUPLEX"
        },
        "page_orientation": {
          "type": "LANDSCAPE"
        },
        "copies": {
          "copies": 5
        },
        "dpi": {
          "horizontal_dpi": 300,
          "vertical_dpi": 400
        },
        "media_size": {
          "width_microns": 210000,
          "height_microns": 297000,
          "vendor_id": "iso_a4_210x297mm"
        },
        "collate": {
          "collate": false
        }
      }
    })";

constexpr char kIncompleteCjt[] = R"(
    {
      "version": "1.0",
      "print": {
        "color": {
          "type": "STANDARD_MONOCHROME"
        },
        "duplex": {
          "type": "NO_DUPLEX"
        },
        "copies": {
          "copies": 5
        },
        "dpi": {
          "horizontal_dpi": 300,
          "vertical_dpi": 400
        }
      }
    })";

constexpr char kPdfExample[] =
    "%PDF- This is a string starting with a PDF's magic bytes and long enough "
    "to be seen as a PDF by LooksLikePdf.";

constexpr char kPngExample[] =
    "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x02\x00\x00\x00\x02\x08"
    "\x02\x00\x00\x00\xfd\xd4\x9as\x00\x00\x00\x16IDAT\x08\xd7"
    "c\xfc\xcf\xc0"
    "\xc0\xc0\xc0\xc0\xc4\xc0\xc0\xc0\xc0\xc0\x00\x00\r\x1d\x01\x03+\xe9\xa6"
    "\xc8\x00\x00\x00\x00IEND\xae"
    "B`\x82";
constexpr size_t kPngExampleSize = 79;

std::optional<api::printing::SubmitJob::Params> ConstructSubmitJobParams(
    const std::string& printer_id,
    const std::string& title,
    const std::string& ticket,
    const std::string& content_type,
    std::optional<std::string> document_blob_uuid) {
  api::printing::SubmitJobRequest request;
  request.job.printer_id = printer_id;
  request.job.title = title;
  if (auto result = api::printer_provider::PrintJob::Ticket::FromValue(
          base::test::ParseJsonDict(ticket))) {
    request.job.ticket = std::move(result).value();
  } else {
    ADD_FAILURE() << "Failed to parse ticket \"" << ticket << "\".";
  }
  request.job.content_type = content_type;
  request.document_blob_uuid = std::move(document_blob_uuid);

  base::Value::List args;
  args.Append(base::Value(request.ToValue()));
  return api::printing::SubmitJob::Params::Create(args);
}

std::optional<printing::PrinterSemanticCapsAndDefaults>
ConstructPrinterCapabilities() {
  printing::PrinterSemanticCapsAndDefaults capabilities;
  capabilities.color_model = printing::mojom::ColorModel::kColor;
  capabilities.duplex_modes.push_back(printing::mojom::DuplexMode::kSimplex);
  capabilities.copies_max = 5;
  capabilities.dpis.emplace_back(kHorizontalDpi, kVerticalDpi);
  printing::PrinterSemanticCapsAndDefaults::Paper paper(
      /*display_name=*/"", kMediaSizeVendorId,
      {kMediaSizeWidth, kMediaSizeHeight});
  capabilities.papers.push_back(std::move(paper));
  capabilities.collate_capable = true;
  return capabilities;
}

std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob(
    content::BrowserContext* browser_context,
    const std::string& content,
    const std::string& content_type) {
  base::test::TestFuture<std::unique_ptr<content::BlobHandle>> blob_future;
  browser_context->CreateMemoryBackedBlob(
      base::as_bytes(base::make_span(content)), content_type,
      blob_future.GetCallback());
  return blob_future.Take();
}

using GetPrintersFuture =
    base::test::TestFuture<std::vector<api::printing::Printer>>;
using GetPrinterInfoFuture =
    base::test::TestFuture<std::optional<base::Value>,
                           std::optional<api::printing::PrinterStatus>,
                           std::optional<std::string>>;
using SubmitJobFuture =
    base::test::TestFuture<std::optional<api::printing::SubmitJobStatus>,
                           std::optional<std::string>,
                           std::optional<std::string>>;

}  // namespace

class TestLocalPrinter : public FakeLocalPrinter {
 public:
  struct JobInfo {
    std::string printer_id;
    unsigned int job_id;
  };

  TestLocalPrinter() = default;
  TestLocalPrinter(TestLocalPrinter&) = delete;
  TestLocalPrinter& operator=(const TestLocalPrinter&) = delete;
  ~TestLocalPrinter() override { EXPECT_TRUE(print_jobs_.empty()); }

  std::vector<JobInfo> jobs_cancelled() { return jobs_cancelled_; }

  std::vector<crosapi::mojom::PrintJobPtr> TakePrintJobs() {
    std::vector<crosapi::mojom::PrintJobPtr> print_jobs;
    std::swap(print_jobs, print_jobs_);
    return print_jobs;
  }

  void AddPrinter(crosapi::mojom::LocalDestinationInfoPtr printer) {
    printers_.push_back(std::move(printer));
  }

  void SetCaps(const std::string& id,
               crosapi::mojom::CapabilitiesResponsePtr caps) {
    DCHECK(caps);
    caps_map_[id] = std::move(caps);
  }

  void CreatePrintJob(crosapi::mojom::PrintJobPtr job,
                      CreatePrintJobCallback cb) override {
    print_jobs_.push_back(std::move(job));
    std::move(cb).Run();
  }

  void GetPrinters(GetPrintersCallback cb) override {
    std::vector<crosapi::mojom::LocalDestinationInfoPtr> printers;
    std::swap(printers, printers_);
    std::move(cb).Run(std::move(printers));
  }

  void GetCapability(const std::string& id, GetCapabilityCallback cb) override {
    auto it = caps_map_.find(id);
    if (it == caps_map_.end()) {
      std::move(cb).Run(nullptr);
      return;
    }
    std::move(cb).Run(std::move(it->second));
    caps_map_.erase(it);
  }

  void CancelPrintJob(const std::string& printer_id,
                      unsigned int job_id,
                      CancelPrintJobCallback cb) override {
    jobs_cancelled_.push_back(JobInfo{printer_id, job_id});
    std::move(cb).Run(true);
  }

 private:
  std::vector<crosapi::mojom::PrintJobPtr> print_jobs_;
  std::vector<JobInfo> jobs_cancelled_;
  std::map<std::string, crosapi::mojom::CapabilitiesResponsePtr> caps_map_;
  std::vector<crosapi::mojom::LocalDestinationInfoPtr> printers_;
};

class PrintingAPIHandlerUnittest : public testing::Test {
 public:
  PrintingAPIHandlerUnittest()
      : disable_pdf_flattening_reset_(
            PrintJobSubmitter::DisablePdfFlatteningForTesting()) {}
  PrintingAPIHandlerUnittest(const PrintingAPIHandlerUnittest&) = delete;
  PrintingAPIHandlerUnittest& operator=(const PrintingAPIHandlerUnittest&) =
      delete;
  ~PrintingAPIHandlerUnittest() override = default;

  std::vector<crosapi::mojom::PrintJobPtr> TakePrintJobs() {
    return local_printer_.TakePrintJobs();
  }

  std::vector<TestLocalPrinter::JobInfo> GetJobsCancelled() {
    return local_printer_.jobs_cancelled();
  }

  void AddPrinter(const crosapi::mojom::LocalDestinationInfo& printer) {
    local_printer_.AddPrinter(printer.Clone());
  }

  void SetCaps(const std::string& id,
               crosapi::mojom::CapabilitiesResponsePtr caps) {
    local_printer_.SetCaps(id, std::move(caps));
  }

  std::string SubmitJob(std::string document_data = kPdfExample,
                        const char* content_type = "application/pdf") {
    auto caps = crosapi::mojom::CapabilitiesResponse::New();
    caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
    caps->capabilities = ConstructPrinterCapabilities();
    SetCaps(kPrinterId, std::move(caps));

    // Create Blob with given data.
    std::unique_ptr<content::BlobHandle> blob = CreateMemoryBackedBlob(
        testing_profile_, document_data, /*content_type=*/"");
    auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt,
                                           content_type, blob->GetUUID());
    EXPECT_TRUE(params);

    SubmitJobFuture job_future;
    printing_api_handler_->SubmitJob(
        /*native_window=*/nullptr, extension_, std::move(params),
        job_future.GetCallback());

    auto [submit_job_status, job_id, error] = job_future.Take();
    EXPECT_FALSE(error);
    EXPECT_TRUE(job_id);
    EXPECT_TRUE(submit_job_status);
    EXPECT_EQ(api::printing::SubmitJobStatus::kOk, submit_job_status);
    // Only lacros needs to report the print job to ash chrome.
#if BUILDFLAG(IS_CHROMEOS_LACROS)
    EXPECT_EQ(1u, TakePrintJobs().size());
#else
    EXPECT_EQ(0u, TakePrintJobs().size());
#endif
    return *job_id;
  }

  void SetUp() override {
    profile_manager_ = std::make_unique<TestingProfileManager>(
        TestingBrowserProcess::GetGlobal());
    ASSERT_TRUE(profile_manager_->SetUp());
    testing_profile_ =
        profile_manager_->CreateTestingProfile(chrome::kInitialProfile);

    base::Value::List extensions_list;
    extensions_list.Append(kExtensionId);
    testing_profile_->GetTestingPrefService()->SetList(
        prefs::kPrintingAPIExtensionsAllowlist, std::move(extensions_list));

    const char kExtensionName[] = "Printing extension";
    const char kPermissionName[] = "printing";
    extension_ = ExtensionBuilder(kExtensionName)
                     .SetID(kExtensionId)
                     .AddAPIPermission(kPermissionName)
                     .Build();
    ExtensionRegistry::Get(testing_profile_)->AddEnabled(extension_);

    auto print_job_controller = std::make_unique<FakePrintJobController>();
    print_job_controller_ = print_job_controller.get();
    auto cups_wrapper = std::make_unique<chromeos::TestCupsWrapper>();
    cups_wrapper_ = cups_wrapper.get();
    event_router_ = CreateAndUseTestEventRouter(testing_profile_);

    printing_api_handler_ = PrintingAPIHandler::CreateForTesting(
        testing_profile_, event_router_,
        ExtensionRegistry::Get(testing_profile_),
        std::move(print_job_controller), std::move(cups_wrapper),
        &local_printer_);
  }

  void TearDown() override {
    cups_wrapper_ = nullptr;
    print_job_controller_ = nullptr;
    printing_api_handler_.reset();
    event_router_ = nullptr;
    testing_profile_ = nullptr;
    profile_manager_->DeleteTestingProfile(chrome::kInitialProfile);
    profile_manager_.reset();
  }

 protected:
  content::BrowserTaskEnvironment task_environment_;
  raw_ptr<TestingProfile> testing_profile_ = nullptr;
  raw_ptr<TestEventRouter> event_router_ = nullptr;
  raw_ptr<FakePrintJobController> print_job_controller_ = nullptr;
  raw_ptr<chromeos::TestCupsWrapper> cups_wrapper_ = nullptr;
  std::unique_ptr<PrintingAPIHandler> printing_api_handler_;
  scoped_refptr<const Extension> extension_;

 private:
  TestLocalPrinter local_printer_;
  // Resets `disable_pdf_flattening_for_testing` back to false automatically
  // after the test is over.
  base::AutoReset<bool> disable_pdf_flattening_reset_;
  std::unique_ptr<TestingProfileManager> profile_manager_;
};

struct Param {
  crosapi::mojom::PrintJobStatus status;
  api::printing::JobStatus expected_status;
};

class PrintingAPIHandlerParam : public PrintingAPIHandlerUnittest,
                                public testing::WithParamInterface<Param> {};

INSTANTIATE_TEST_SUITE_P(
    All,
    PrintingAPIHandlerParam,
    testing::Values(Param{crosapi::mojom::PrintJobStatus::kUnknown,
                          api::printing::JobStatus::kPending},
                    Param{crosapi::mojom::PrintJobStatus::kStarted,
                          api::printing::JobStatus::kInProgress},
                    Param{crosapi::mojom::PrintJobStatus::kDone,
                          api::printing::JobStatus::kPrinted},
                    Param{crosapi::mojom::PrintJobStatus::kError,
                          api::printing::JobStatus::kFailed},
                    Param{crosapi::mojom::PrintJobStatus::kCancelled,
                          api::printing::JobStatus::kCanceled}));

// Test that `OnJobStatusChanged` is dispatched when the print job status is
// changed.
TEST_P(PrintingAPIHandlerParam, EventIsDispatched) {
  PrintingEventObserver event_observer(
      event_router_, api::printing::OnJobStatusChanged::kEventName);
  const auto job_id = SubmitJob();
  ASSERT_TRUE(job_id.size() > 1);
  int index = job_id.size() - 1;
  const Param& param = GetParam();
  if (param.status != crosapi::mojom::PrintJobStatus::kUnknown) {
    auto update = crosapi::mojom::PrintJobUpdate::New();
    update->status = param.status;
    printing_api_handler_->OnPrintJobUpdate(
        job_id.substr(0, index), job_id[index] - '0', std::move(update));
  }

  event_observer.CheckJobStatusEvent(kExtensionId, job_id,
                                     param.expected_status);
}

// Test that calling GetPrinters() returns no printers before any are added to
// the profile.
TEST_F(PrintingAPIHandlerUnittest, GetPrinters_NoPrinters) {
  GetPrintersFuture printers_future;
  printing_api_handler_->GetPrinters(printers_future.GetCallback());
  EXPECT_TRUE(printers_future.Get().empty());
}

// Test that calling GetPrinters() returns the mock printer.
TEST_F(PrintingAPIHandlerUnittest, GetPrinters_OnePrinter) {
  AddPrinter(crosapi::mojom::LocalDestinationInfo(
      kId1, kName, kDescription, true, std::make_optional(kUri)));

  GetPrintersFuture printers_future;
  printing_api_handler_->GetPrinters(printers_future.GetCallback());

  auto printers = printers_future.Take();
  ASSERT_EQ(1u, printers.size());
  const api::printing::Printer& idl_printer = printers.front();

  EXPECT_EQ(kId1, idl_printer.id);
  EXPECT_EQ(kName, idl_printer.name);
  EXPECT_EQ(kDescription, idl_printer.description);
  EXPECT_EQ(kUri, idl_printer.uri);
  EXPECT_EQ(api::printing::PrinterSource::kPolicy, idl_printer.source);
  EXPECT_FALSE(idl_printer.is_default);
  EXPECT_EQ(std::nullopt, idl_printer.recently_used_rank);
}

// Test that calling GetPrinters() returns printers with correct `is_default`
// flag.
TEST_F(PrintingAPIHandlerUnittest, GetPrinters_IsDefault) {
  testing_profile_->GetPrefs()->SetString(
      prefs::kPrintPreviewDefaultDestinationSelectionRules,
      R"({"kind": "local", "idPattern": "id.*"})");
  AddPrinter(crosapi::mojom::LocalDestinationInfo(
      kId1, kName, kDescription, true, std::make_optional(kUri)));

  GetPrintersFuture printers_future;
  printing_api_handler_->GetPrinters(printers_future.GetCallback());

  auto printers = printers_future.Take();
  ASSERT_EQ(1u, printers.size());
  api::printing::Printer idl_printer = std::move(printers.front());

  EXPECT_EQ(kId1, idl_printer.id);
  EXPECT_TRUE(idl_printer.is_default);
}

// Test that calling GetPrinters() returns printers with correct
// `recently_used_rank` flag.
TEST_F(PrintingAPIHandlerUnittest, GetPrinters_RecentlyUsedRank) {
  printing::PrintPreviewStickySettings* sticky_settings =
      printing::PrintPreviewStickySettings::GetInstance();
  sticky_settings->StoreAppState(R"({
    "version": 2,
    "recentDestinations": [
      {
        "id": "id3"
      },
      {
        "id": "id1"
      }
    ]
  })");
  sticky_settings->SaveInPrefs(testing_profile_->GetPrefs());
  AddPrinter(crosapi::mojom::LocalDestinationInfo(
      kId1, kName, kDescription, true, std::make_optional(kUri)));

  GetPrintersFuture printers_future;
  printing_api_handler_->GetPrinters(printers_future.GetCallback());

  auto printers = printers_future.Take();
  ASSERT_EQ(1u, printers.size());
  api::printing::Printer idl_printer = std::move(printers.front());

  EXPECT_EQ(kId1, idl_printer.id);
  ASSERT_TRUE(idl_printer.recently_used_rank);
  // The "id1" printer is listed as second printer in the recently used printers
  // list, so we expect 1 as its rank.
  EXPECT_EQ(1, *idl_printer.recently_used_rank);
}

TEST_F(PrintingAPIHandlerUnittest, GetPrinterInfo_InvalidId) {
  GetPrinterInfoFuture printer_info_future;
  printing_api_handler_->GetPrinterInfo(kPrinterId,
                                        printer_info_future.GetCallback());

  auto [capabilities, printer_status, error] = printer_info_future.Take();
  // Printer is not added to CupsPrintersManager, so we expect "Invalid printer
  // id" error.
  EXPECT_FALSE(capabilities);
  EXPECT_FALSE(printer_status);
  ASSERT_TRUE(error);
  EXPECT_EQ("Invalid printer ID", error);
}

TEST_F(PrintingAPIHandlerUnittest, GetPrinterInfo_NoCapabilities) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  SetCaps(kPrinterId, std::move(caps));

  GetPrinterInfoFuture printer_info_future;
  printing_api_handler_->GetPrinterInfo(kPrinterId,
                                        printer_info_future.GetCallback());

  auto [capabilities, printer_status, error] = printer_info_future.Take();
  EXPECT_FALSE(capabilities);
  ASSERT_TRUE(printer_status);
  EXPECT_EQ(api::printing::PrinterStatus::kUnreachable, printer_status);
  EXPECT_FALSE(error);
}

TEST_F(PrintingAPIHandlerUnittest, GetPrinterInfo_OutOfPaper) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  caps->capabilities = printing::PrinterSemanticCapsAndDefaults();
  SetCaps(kPrinterId, std::move(caps));

  // Mock CUPS wrapper to return predefined status for given printer.
  printing::PrinterStatus::PrinterReason reason{
      printing::PrinterStatus::PrinterReason::Reason::kMediaEmpty,
      printing::PrinterStatus::PrinterReason::Severity::kWarning};
  cups_wrapper_->SetPrinterStatus(kPrinterId, reason);

  std::map<std::string, crosapi::mojom::PrinterStatusPtr> status_map_;

  GetPrinterInfoFuture printer_info_future;
  printing_api_handler_->GetPrinterInfo(kPrinterId,
                                        printer_info_future.GetCallback());

  auto [capabilities, printer_status, error] = printer_info_future.Take();
  ASSERT_TRUE(capabilities);
  const base::Value::Dict* capabilities_dict =
      capabilities->GetDict().FindDict("printer");
  ASSERT_TRUE(capabilities_dict);

  const base::Value::Dict* color = capabilities_dict->FindDict("color");
  ASSERT_TRUE(color);
  const base::Value::List* color_options = color->FindList("option");
  ASSERT_TRUE(color_options);
  ASSERT_EQ(1u, color_options->size());
  const std::string* color_type =
      (*color_options)[0].GetDict().FindString("type");
  ASSERT_TRUE(color_type);
  EXPECT_EQ("STANDARD_MONOCHROME", *color_type);

  const base::Value::Dict* page_orientation =
      capabilities_dict->FindDict("page_orientation");
  ASSERT_TRUE(page_orientation);
  const base::Value::List* page_orientation_options =
      page_orientation->FindList("option");
  ASSERT_TRUE(page_orientation_options);
  ASSERT_EQ(3u, page_orientation_options->size());
  std::vector<std::string> page_orientation_types;
  for (const base::Value& page_orientation_option : *page_orientation_options) {
    const std::string* page_orientation_type =
        page_orientation_option.GetDict().FindString("type");
    ASSERT_TRUE(page_orientation_type);
    page_orientation_types.push_back(*page_orientation_type);
  }
  EXPECT_THAT(page_orientation_types,
              testing::UnorderedElementsAre("PORTRAIT", "LANDSCAPE", "AUTO"));

  ASSERT_TRUE(printer_status);
  EXPECT_EQ(api::printing::PrinterStatus::kOutOfPaper, printer_status);
  EXPECT_FALSE(error);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_UnsupportedContentType) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  caps->capabilities = printing::PrinterSemanticCapsAndDefaults();
  SetCaps(kPrinterId, std::move(caps));

  auto params =
      ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt, "image/jpeg",
                               /*document_blob_uuid=*/std::nullopt);
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  // According to the documentation only "application/pdf" content type is
  // supported, so we expect an error as a result of API call.
  ASSERT_TRUE(error);
  EXPECT_EQ("Unsupported content type", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_InvalidPrintTicket) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  caps->capabilities = ConstructPrinterCapabilities();
  SetCaps(kPrinterId, std::move(caps));

  auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"",
                                         kIncompleteCjt, "application/pdf",
                                         /*document_blob_uuid=*/std::nullopt);
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  // Some fields of the print ticket are missing, so we expect an error as a
  // result of API call.
  ASSERT_TRUE(error);
  EXPECT_EQ("Invalid ticket", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_InvalidPrinterId) {
  auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt,
                                         "application/pdf",
                                         /*document_blob_uuid=*/std::nullopt);
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  // The printer is not added, so we expect an error as a result of API call.
  ASSERT_TRUE(error);
  EXPECT_EQ("Invalid printer ID", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_PrinterUnavailable) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  SetCaps(kPrinterId, std::move(caps));

  auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt,
                                         "application/pdf",
                                         /*document_blob_uuid=*/std::nullopt);
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  // Even though the printer is added, it's not able to accept jobs until it's
  // added as valid printer, so we expect an error as a result of API call.
  ASSERT_TRUE(error);
  EXPECT_EQ("Printer is unavailable at the moment", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_UnsupportedTicket) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  caps->capabilities = printing::PrinterSemanticCapsAndDefaults();
  SetCaps(kPrinterId, std::move(caps));

  auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt,
                                         "application/pdf",
                                         /*document_blob_uuid=*/std::nullopt);
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  // Print ticket requires some non-default parameters as DPI and media size
  // which are not supported for default capabilities, so we expect an error as
  // a result of API call.
  ASSERT_TRUE(error);
  EXPECT_EQ("Ticket is unsupported on the given printer", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_InvalidData) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  caps->capabilities = ConstructPrinterCapabilities();
  SetCaps(kPrinterId, std::move(caps));

  auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt,
                                         "application/pdf", "invalid_uuid");
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  // We can't fetch actual document data without Blob UUID, so we expect an
  // error as a result of API call.
  ASSERT_TRUE(error);
  EXPECT_EQ("Invalid document", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_InvalidDataPNG) {
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  caps->capabilities = ConstructPrinterCapabilities();
  SetCaps(kPrinterId, std::move(caps));

  auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt,
                                         "image/png", "invalid_uuid");
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  // We can't fetch actual document data without Blob UUID, so we expect an
  // error as a result of API call.
  ASSERT_TRUE(error);
  EXPECT_EQ("Invalid document", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_PrintingFailed) {
  print_job_controller_->set_fail(true);
  auto caps = crosapi::mojom::CapabilitiesResponse::New();
  caps->basic_info = crosapi::mojom::LocalDestinationInfo::New();
  caps->capabilities = ConstructPrinterCapabilities();
  SetCaps(kPrinterId, std::move(caps));

  // Create Blob with given data.
  std::unique_ptr<content::BlobHandle> blob = CreateMemoryBackedBlob(
      testing_profile_, kPdfExample, /*content_type=*/"");
  auto params = ConstructSubmitJobParams(kPrinterId, /*title=*/"", kCjt,
                                         "application/pdf", blob->GetUUID());
  ASSERT_TRUE(params);

  SubmitJobFuture job_future;
  printing_api_handler_->SubmitJob(
      /*native_window=*/nullptr, extension_, std::move(params),
      job_future.GetCallback());

  auto [submit_job_status, job_id, error] = job_future.Take();
  ASSERT_TRUE(error);
  EXPECT_EQ("Printing failed", error);
  EXPECT_FALSE(submit_job_status);
  EXPECT_FALSE(job_id);
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob) {
  SubmitJob();
}

TEST_F(PrintingAPIHandlerUnittest, SubmitJob_PNG) {
  SubmitJob(std::string(kPngExample, kPngExampleSize), "image/png");
}

TEST_F(PrintingAPIHandlerUnittest, CancelJob_InvalidId) {
  std::optional<std::string> error =
      printing_api_handler_->CancelJob(kExtensionId, "job_id");

  ASSERT_TRUE(error);
  EXPECT_EQ("No active print job with given ID", error);
  EXPECT_TRUE(GetJobsCancelled().empty());
}

TEST_F(PrintingAPIHandlerUnittest, CancelJob_InvalidId_OtherExtension) {
  const auto job_id = SubmitJob();

  // Try to cancel print job from other extension.
  std::optional<std::string> error =
      printing_api_handler_->CancelJob(kExtensionId2, job_id);

  ASSERT_TRUE(error);
  EXPECT_EQ("No active print job with given ID", error);
  EXPECT_TRUE(GetJobsCancelled().empty());
}

TEST_F(PrintingAPIHandlerUnittest, CancelJob_InvalidState) {
  const auto job_id = SubmitJob();

  // Explicitly complete started print job.
  ASSERT_TRUE(job_id.size() > 1);
  int index = job_id.size() - 1;
  auto update = crosapi::mojom::PrintJobUpdate::New();
  update->status = crosapi::mojom::PrintJobStatus::kDone;
  printing_api_handler_->OnPrintJobUpdate(
      job_id.substr(0, index), job_id[index] - '0', std::move(update));

  // Try to cancel already completed print job.
  std::optional<std::string> error =
      printing_api_handler_->CancelJob(kExtensionId, job_id);

  ASSERT_TRUE(error);
  EXPECT_EQ("No active print job with given ID", error);
  EXPECT_TRUE(GetJobsCancelled().empty());
}

TEST_F(PrintingAPIHandlerUnittest, CancelJob) {
  const auto job_id = SubmitJob();

  PrintingEventObserver event_observer(
      event_router_, api::printing::OnJobStatusChanged::kEventName);

  // Cancel started print job.
  std::optional<std::string> error =
      printing_api_handler_->CancelJob(kExtensionId, job_id);

  EXPECT_FALSE(error);
  ASSERT_EQ(1u, GetJobsCancelled().size());
  EXPECT_EQ(job_id,
            PrintingAPIHandler::CreateUniqueId(GetJobsCancelled()[0].printer_id,
                                               GetJobsCancelled()[0].job_id));
  // Job should not be canceled yet.
  EXPECT_EQ("", event_observer.extension_id());
  EXPECT_TRUE(event_observer.event_args().is_none());

  auto update = crosapi::mojom::PrintJobUpdate::New();
  update->status = crosapi::mojom::PrintJobStatus::kCancelled;
  printing_api_handler_->OnPrintJobUpdate(GetJobsCancelled()[0].printer_id,
                                          GetJobsCancelled()[0].job_id,
                                          std::move(update));

  // Now the job is canceled.
  event_observer.CheckJobStatusEvent(kExtensionId, job_id,
                                     api::printing::JobStatus::kCanceled);
}

}  // namespace extensions