chromium/printing/printing_context_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 "printing/printing_context_chromeos.h"

#include <string>

#include "base/memory/raw_ptr.h"
#include "base/strings/string_split.h"
#include "base/strings/utf_string_conversions.h"
#include "printing/backend/cups_ipp_constants.h"
#include "printing/backend/mock_cups_printer.h"
#include "printing/mojom/print.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace printing {

namespace {

using ::testing::_;
using ::testing::ByMove;
using ::testing::DoAll;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::SetArgPointee;

constexpr char kPrinterName[] = "printer";
constexpr char16_t kPrinterName16[] = u"printer";

constexpr char kUsername[] = "test user";

constexpr char kDocumentName[] = "document name";
constexpr char16_t kDocumentName16[] = u"document name";

constexpr gfx::Size kDefaultPaperSize = {215900, 279400};
constexpr char kDefaultPaperName[] = "some_vendor_id";

class MockCupsConnection : public CupsConnection {
 public:
  MOCK_METHOD1(GetDests, bool(std::vector<std::unique_ptr<CupsPrinter>>&));
  MOCK_METHOD2(GetJobs,
               bool(const std::vector<std::string>& printer_ids,
                    std::vector<QueueStatus>* jobs));
  MOCK_METHOD2(GetPrinterStatus,
               bool(const std::string& printer_id,
                    PrinterStatus* printer_status));
  MOCK_CONST_METHOD0(server_name, std::string());
  MOCK_CONST_METHOD0(last_error, int());
  MOCK_CONST_METHOD0(last_error_message, std::string());

  MOCK_METHOD1(GetPrinter,
               std::unique_ptr<CupsPrinter>(const std::string& printer_name));
};

class TestPrintSettings : public PrintSettings {
 public:
  TestPrintSettings() { set_duplex_mode(mojom::DuplexMode::kSimplex); }
};

class PrintingContextTest : public testing::Test,
                            public PrintingContext::Delegate {
 public:
  void SetDefaultSettings(bool send_user_info, const std::string& uri) {
    auto unique_connection = std::make_unique<MockCupsConnection>();
    auto* connection = unique_connection.get();
    auto unique_printer = std::make_unique<NiceMock<MockCupsPrinter>>();
    printer_ = unique_printer.get();
    EXPECT_CALL(*printer_, GetUri()).WillRepeatedly(Return(uri));
    EXPECT_CALL(*connection, GetPrinter(kPrinterName))
        .WillOnce(Return(ByMove(std::move(unique_printer))));
    printing_context_ = PrintingContextChromeos::CreateForTesting(
        this, PrintingContext::ProcessBehavior::kOopDisabled,
        std::move(unique_connection));
    auto settings = std::make_unique<PrintSettings>();
    settings->set_device_name(kPrinterName16);
    settings->set_send_user_info(send_user_info);
    settings->set_duplex_mode(mojom::DuplexMode::kLongEdge);
    settings->set_username(kUsername);
    printing_context_->UpdatePrintSettingsFromPOD(std::move(settings));
    settings_.set_requested_media({kDefaultPaperSize, kDefaultPaperName});
  }

  ipp_attribute_t* GetAttribute(ipp_t* attributes,
                                const char* attr_name) const {
    DCHECK(attr_name);
    ipp_attribute_t* ret = nullptr;
    for (ipp_attribute_t* attr = ippFirstAttribute(attributes); attr;
         attr = ippNextAttribute(attributes)) {
      const char* name = ippGetName(attr);
      if (name && !strcmp(attr_name, name)) {
        EXPECT_EQ(nullptr, ret)
            << "Multiple attributes with name " << attr_name << " found.";
        ret = attr;
      }
    }
    EXPECT_TRUE(ret);
    return ret;
  }

  void TestStringOptionValue(const char* attr_name,
                             const char* expected_value) const {
    auto attributes = SettingsToIPPOptions(settings_, printable_area_);
    auto* attr = GetAttribute(attributes.get(), attr_name);
    EXPECT_STREQ(expected_value, ippGetString(attr, 0, nullptr));
  }

  void TestIntegerOptionValue(const char* attr_name, int expected_value) const {
    auto attributes = SettingsToIPPOptions(settings_, printable_area_);
    auto* attr = GetAttribute(attributes.get(), attr_name);
    EXPECT_EQ(expected_value, ippGetInteger(attr, 0));
  }

  void TestOctetStringOptionValue(const char* attr_name,
                                  base::span<const char> expected_value) const {
    auto attributes = SettingsToIPPOptions(settings_, printable_area_);
    auto* attr = GetAttribute(attributes.get(), attr_name);
    int length;
    void* value = ippGetOctetString(attr, 0, &length);
    ASSERT_EQ(expected_value.size(), static_cast<size_t>(length));
    ASSERT_TRUE(value);
    EXPECT_EQ(0, memcmp(expected_value.data(), value, expected_value.size()));
  }

  void TestResolutionOptionValue(const char* attr_name,
                                 int expected_x_res,
                                 int expected_y_res) const {
    auto attributes = SettingsToIPPOptions(settings_, printable_area_);
    auto* attr = GetAttribute(attributes.get(), attr_name);
    ipp_res_t unit;
    int y_res;
    int x_res = ippGetResolution(attr, 0, &y_res, &unit);
    EXPECT_EQ(unit, IPP_RES_PER_INCH);
    EXPECT_EQ(expected_x_res, x_res);
    EXPECT_EQ(expected_y_res, y_res);
  }

  void TestMediaColValue(const gfx::Size& expected_size,
                         int expected_bottom_margin,
                         int expected_left_margin,
                         int expected_right_margin,
                         int expected_top_margin) {
    auto attributes = SettingsToIPPOptions(settings_, printable_area_);
    ipp_t* media_col =
        ippGetCollection(GetAttribute(attributes.get(), kIppMediaCol), 0);
    ipp_t* media_size =
        ippGetCollection(GetAttribute(media_col, kIppMediaSize), 0);

    int width = ippGetInteger(GetAttribute(media_size, kIppXDimension), 0);
    int height = ippGetInteger(GetAttribute(media_size, kIppYDimension), 0);
    EXPECT_EQ(expected_size.width(), width);
    EXPECT_EQ(expected_size.height(), height);

    int bottom =
        ippGetInteger(GetAttribute(media_col, kIppMediaBottomMargin), 0);
    int left = ippGetInteger(GetAttribute(media_col, kIppMediaLeftMargin), 0);
    int right = ippGetInteger(GetAttribute(media_col, kIppMediaRightMargin), 0);
    int top = ippGetInteger(GetAttribute(media_col, kIppMediaTopMargin), 0);

    EXPECT_EQ(expected_bottom_margin, bottom);
    EXPECT_EQ(expected_left_margin, left);
    EXPECT_EQ(expected_right_margin, right);
    EXPECT_EQ(expected_top_margin, top);
  }

  bool HasAttribute(const char* attr_name) const {
    auto attributes = SettingsToIPPOptions(settings_, printable_area_);
    return !!ippFindAttribute(attributes.get(), attr_name, IPP_TAG_ZERO);
  }

  int GetAttrValueCount(const char* attr_name) const {
    auto attributes = SettingsToIPPOptions(settings_, printable_area_);
    auto* attr = GetAttribute(attributes.get(), attr_name);
    return ippGetCount(attr);
  }

  TestPrintSettings settings_;
  gfx::Rect printable_area_;

  // PrintingContext::Delegate methods.
  gfx::NativeView GetParentView() override { return gfx::NativeView(); }
  std::string GetAppLocale() override { return std::string(); }

  std::unique_ptr<PrintingContextChromeos> printing_context_;
  raw_ptr<MockCupsPrinter> printer_;
};

TEST_F(PrintingContextTest, SettingsToIPPOptions_Color) {
  settings_.set_color(mojom::ColorModel::kGray);
  TestStringOptionValue(kIppColor, "monochrome");
  settings_.set_color(mojom::ColorModel::kColor);
  TestStringOptionValue(kIppColor, "color");
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_Duplex) {
  settings_.set_duplex_mode(mojom::DuplexMode::kSimplex);
  TestStringOptionValue(kIppDuplex, "one-sided");
  settings_.set_duplex_mode(mojom::DuplexMode::kLongEdge);
  TestStringOptionValue(kIppDuplex, "two-sided-long-edge");
  settings_.set_duplex_mode(mojom::DuplexMode::kShortEdge);
  TestStringOptionValue(kIppDuplex, "two-sided-short-edge");
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_MediaCol) {
  settings_.set_requested_media(
      {gfx::Size(297000, 420000), "iso_a3_297x420mm"});
  printable_area_ =
      gfx::Rect(2000, 1000, 297000 - (2000 + 3000), 420000 - (1000 + 4000));
  TestMediaColValue(gfx::Size(29700, 42000), 100, 200, 300, 400);
}

TEST_F(PrintingContextTest, SettingsToIPPOptionsMediaColLandscape) {
  settings_.set_requested_media(
      {gfx::Size(148000, 200000), "om_200030x148170um_200x148mm"});
  // Use margins (LBRT) of 500, 700, 200, and 1000.
  printable_area_ =
      gfx::Rect(500, 700, 148000 - (500 + 200), 200000 - (700 + 1000));
  // The requested media and printable area is in portrait mode (height larger
  // than width).  Since the vendor ID has a width larger than the height, the
  // expected media should get swapped.  When swapped, the margins (LBRT) should
  // be 1000, 500, 700, and 200.
  TestMediaColValue(gfx::Size(20000, 14800), 50, 100, 70, 20);
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_Copies) {
  settings_.set_copies(3);
  TestIntegerOptionValue(kIppCopies, 3);
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_Collate) {
  TestStringOptionValue(kIppCollate, "separate-documents-uncollated-copies");
  settings_.set_collate(true);
  TestStringOptionValue(kIppCollate, "separate-documents-collated-copies");
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_Pin) {
  EXPECT_FALSE(HasAttribute(kIppPin));
  settings_.set_pin_value("1234");
  TestOctetStringOptionValue(kIppPin, base::make_span("1234", 4u));
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_Resolution) {
  EXPECT_FALSE(HasAttribute(kIppResolution));
  settings_.set_dpi_xy(0, 300);
  EXPECT_FALSE(HasAttribute(kIppResolution));
  settings_.set_dpi_xy(300, 0);
  EXPECT_FALSE(HasAttribute(kIppResolution));
  settings_.set_dpi(600);
  TestResolutionOptionValue(kIppResolution, 600, 600);
  settings_.set_dpi_xy(600, 1200);
  TestResolutionOptionValue(kIppResolution, 600, 1200);
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_SendUserInfo_Secure) {
  ipp_status_t status = ipp_status_t::IPP_STATUS_OK;
  std::u16string document_name = kDocumentName16;
  SetDefaultSettings(/*send_user_info=*/true, "ipps://test-uri");
  std::string create_job_document_name;
  std::string create_job_username;
  std::string start_document_document_name;
  std::string start_document_username;
  EXPECT_CALL(*printer_, CreateJob)
      .WillOnce(DoAll(SetArgPointee<0>(/*job_id=*/1),
                      SaveArg<1>(&create_job_document_name),
                      SaveArg<2>(&create_job_username), Return(status)));
  EXPECT_CALL(*printer_, StartDocument)
      .WillOnce(DoAll(SaveArg<1>(&start_document_document_name),
                      SaveArg<3>(&start_document_username), Return(true)));

  printing_context_->NewDocument(document_name);

  EXPECT_EQ(create_job_document_name, kDocumentName);
  EXPECT_EQ(start_document_document_name, kDocumentName);
  EXPECT_EQ(create_job_username, kUsername);
  EXPECT_EQ(start_document_username, kUsername);
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_SendUserInfo_Insecure) {
  ipp_status_t status = ipp_status_t::IPP_STATUS_OK;
  std::u16string document_name = kDocumentName16;
  std::string default_username = "chronos";
  std::string default_document_name = "-";
  SetDefaultSettings(/*send_user_info=*/true, "ipp://test-uri");
  std::string create_job_document_name;
  std::string create_job_username;
  std::string start_document_document_name;
  std::string start_document_username;
  EXPECT_CALL(*printer_, CreateJob)
      .WillOnce(DoAll(SetArgPointee<0>(/*job_id=*/1),
                      SaveArg<1>(&create_job_document_name),
                      SaveArg<2>(&create_job_username), Return(status)));
  EXPECT_CALL(*printer_, StartDocument)
      .WillOnce(DoAll(SaveArg<1>(&start_document_document_name),
                      SaveArg<3>(&start_document_username), Return(true)));

  printing_context_->NewDocument(document_name);

  EXPECT_EQ(create_job_document_name, default_document_name);
  EXPECT_EQ(start_document_document_name, default_document_name);
  EXPECT_EQ(create_job_username, default_username);
  EXPECT_EQ(start_document_username, default_username);
}

TEST_F(PrintingContextTest, SettingsToIPPOptions_DoNotSendUserInfo) {
  ipp_status_t status = ipp_status_t::IPP_STATUS_OK;
  std::u16string document_name = kDocumentName16;
  SetDefaultSettings(/*send_user_info=*/false, "ipps://test-uri");
  std::string create_job_document_name;
  std::string create_job_username;
  std::string start_document_document_name;
  std::string start_document_username;
  EXPECT_CALL(*printer_, CreateJob)
      .WillOnce(DoAll(SetArgPointee<0>(/*job_id=*/1),
                      SaveArg<1>(&create_job_document_name),
                      SaveArg<2>(&create_job_username), Return(status)));
  EXPECT_CALL(*printer_, StartDocument)
      .WillOnce(DoAll(SaveArg<1>(&start_document_document_name),
                      SaveArg<3>(&start_document_username), Return(true)));

  printing_context_->NewDocument(document_name);

  EXPECT_EQ(create_job_document_name, "");
  EXPECT_EQ(start_document_document_name, "");
  EXPECT_EQ(create_job_username, "");
  EXPECT_EQ(start_document_username, "");
}

TEST_F(PrintingContextTest, SettingsToIPPOptionsClientInfo) {
  mojom::IppClientInfo client_info(
      mojom::IppClientInfo::ClientType::kOperatingSystem, "a-", "B_", "1.",
      "a.1-B_");
  settings_.set_client_infos({client_info});

  auto attributes = SettingsToIPPOptions(settings_, printable_area_);
  auto* attr = ippFindAttribute(attributes.get(), kIppClientInfo,
                                IPP_TAG_BEGIN_COLLECTION);
  auto* client_info_collection = ippGetCollection(attr, 0);

  attr = ippFindAttribute(client_info_collection, kIppClientName, IPP_TAG_NAME);
  EXPECT_STREQ("a-", ippGetString(attr, 0, nullptr));

  attr = ippFindAttribute(client_info_collection, kIppClientType, IPP_TAG_ENUM);
  EXPECT_EQ(4, ippGetInteger(attr, 0));

  attr =
      ippFindAttribute(client_info_collection, kIppClientPatches, IPP_TAG_TEXT);
  EXPECT_STREQ("B_", ippGetString(attr, 0, nullptr));

  attr = ippFindAttribute(client_info_collection, kIppClientStringVersion,
                          IPP_TAG_TEXT);
  EXPECT_STREQ("1.", ippGetString(attr, 0, nullptr));

  attr = ippFindAttribute(client_info_collection, kIppClientVersion,
                          IPP_TAG_STRING);
  int length;
  void* version = ippGetOctetString(attr, 0, &length);
  ASSERT_TRUE(version);
  EXPECT_EQ(6, length);
  EXPECT_EQ(0, memcmp("a.1-B_", version, 6));
}

TEST_F(PrintingContextTest, SettingsToIPPOptionsClientInfoSomeValid) {
  mojom::IppClientInfo valid_client_info(
      mojom::IppClientInfo::ClientType::kOperatingSystem, "aB.1-_", "aB.1-_",
      "aB.1-_", "aB.1-_");
  mojom::IppClientInfo invalid_client_info(
      mojom::IppClientInfo::ClientType::kOperatingSystem, "{}", "aB.1-_",
      "aB.1-_", "aB.1-_");
  settings_.set_client_infos(
      {valid_client_info, invalid_client_info, valid_client_info});

  // Check that the invalid item is skipped in the client-info collection.
  EXPECT_EQ(GetAttrValueCount(kIppClientInfo), 2);
}

TEST_F(PrintingContextTest, SettingsToIPPOptionsClientInfoEmpty) {
  settings_.set_client_infos({});
  EXPECT_FALSE(HasAttribute(kIppClientInfo));

  mojom::IppClientInfo invalid_client_info(
      mojom::IppClientInfo::ClientType::kOther, "$", " ", "{}", std::nullopt);

  settings_.set_client_infos({invalid_client_info});
  EXPECT_FALSE(HasAttribute(kIppClientInfo));
}

}  // namespace

}  // namespace printing