// 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