blob: f286dae32b05eb3d8d113ce4ad7c669f577daa0e [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/printing/common/print_dialog_linux_portal.h"
#include <memory>
#include <string>
#include <vector>
#include "base/files/file.h"
#include "base/files/file_descriptor_watcher_posix.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/memory/scoped_refptr.h"
#include "base/posix/eintr_wrapper.h"
#include "base/run_loop.h"
#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "components/dbus/utils/variant.h"
#include "components/dbus/utils/write_value.h"
#include "components/dbus/xdg/portal.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "printing/metafile.h"
#include "printing/printing_context_linux.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/resource/resource_bundle.h"
using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
namespace printing {
namespace {
class MockPrintingContextDelegate : public PrintingContext::Delegate {
public:
MOCK_METHOD(gfx::NativeView, GetParentView, (), (override));
MOCK_METHOD(std::string, GetAppLocale, (), (override));
};
class MockMetafile : public MetafilePlayer {
public:
MOCK_CONST_METHOD1(GetDataAsVector, bool(std::vector<char>* buffer));
MOCK_CONST_METHOD0(GetDataAsSharedMemoryRegion, base::MappedReadOnlyRegion());
MOCK_CONST_METHOD0(ShouldCopySharedMemoryRegionData, bool());
MOCK_CONST_METHOD0(GetDataType, mojom::MetafileDataType());
MOCK_CONST_METHOD1(SaveTo, bool(base::File* file));
};
} // namespace
class PrintDialogLinuxPortalTest : public testing::Test {
public:
void SetUp() override {
if (!ui::ResourceBundle::HasSharedInstance()) {
ui::ResourceBundle::InitSharedInstanceWithLocale(
"en-US", nullptr, ui::ResourceBundle::DO_NOT_LOAD_COMMON_RESOURCES);
resource_bundle_initialized_ = true;
}
mock_bus_ = base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
mock_proxy_ = base::MakeRefCounted<dbus::MockObjectProxy>(
mock_bus_.get(), "org.freedesktop.portal.Desktop",
dbus::ObjectPath("/org/freedesktop/portal/desktop"));
EXPECT_CALL(*mock_bus_, GetConnectionName())
.WillRepeatedly(Return(":1.23"));
EXPECT_CALL(*mock_bus_, IsConnected()).WillRepeatedly(Return(true));
EXPECT_CALL(*mock_bus_, GetObjectProxy(_, _))
.WillRepeatedly(Return(mock_proxy_.get()));
EXPECT_CALL(*mock_bus_, AssertOnOriginThread()).WillRepeatedly([] {});
EXPECT_CALL(*mock_bus_, GetDBusTaskRunner())
.WillRepeatedly(
Return(task_environment_.GetMainThreadTaskRunner().get()));
EXPECT_CALL(*mock_bus_, GetOriginTaskRunner())
.WillRepeatedly(
Return(task_environment_.GetMainThreadTaskRunner().get()));
delegate_ = std::make_unique<MockPrintingContextDelegate>();
context_ = std::make_unique<PrintingContextLinux>(
delegate_.get(), PrintingContext::OutOfProcessBehavior::kDisabled);
}
void TearDown() override {
dialog_ = nullptr;
context_.reset();
dbus_xdg::SetPortalStateForTesting(dbus_xdg::PortalRegistrarState::kIdle);
if (resource_bundle_initialized_) {
ui::ResourceBundle::CleanupSharedInstance();
}
}
protected:
bool resource_bundle_initialized_ = false;
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
scoped_refptr<dbus::MockBus> mock_bus_;
scoped_refptr<dbus::MockObjectProxy> mock_proxy_;
std::unique_ptr<MockPrintingContextDelegate> delegate_;
std::unique_ptr<PrintingContextLinux> context_;
std::unique_ptr<PrintDialogLinuxPortal> dialog_;
};
TEST_F(PrintDialogLinuxPortalTest, ShowDialog_PortalAvailable) {
dbus_xdg::SetPortalStateForTesting(dbus_xdg::PortalRegistrarState::kSuccess);
dialog_ = std::make_unique<PrintDialogLinuxPortal>(context_.get(), mock_bus_);
dbus::ObjectProxy::ResponseOrErrorCallback prepare_print_callback;
dbus::ObjectProxy::SignalCallback signal_callback_captured;
// Expect PreparePrint call
EXPECT_CALL(*mock_proxy_,
CallMethodWithErrorResponse(
testing::ResultOf(
[](dbus::MethodCall* call) { return call->GetMember(); },
"PreparePrint"),
_, _))
.WillOnce([&prepare_print_callback](
dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseOrErrorCallback callback) {
prepare_print_callback = std::move(callback);
});
// Expect Request Response signal connection
EXPECT_CALL(*mock_proxy_, ConnectToSignal("org.freedesktop.portal.Request",
"Response", _, _))
.WillRepeatedly(
[&signal_callback_captured](
const std::string& interface_name, const std::string& signal_name,
dbus::ObjectProxy::SignalCallback signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback) {
signal_callback_captured = signal_callback;
std::move(on_connected_callback)
.Run(interface_name, signal_name, true);
});
base::RunLoop run_loop;
dialog_->ShowDialog(
nullptr, false,
base::BindOnce(
[](base::RunLoop* run_loop, mojom::ResultCode result) {
EXPECT_EQ(result, mojom::ResultCode::kSuccess);
run_loop->Quit();
},
&run_loop));
ASSERT_TRUE(prepare_print_callback);
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendObjectPath(
dbus::ObjectPath("/org/freedesktop/portal/request/1"));
std::move(prepare_print_callback).Run(response.get(), nullptr);
// Now OnMethodResponse should have run, updated path, and connected signal
// again.
ASSERT_TRUE(signal_callback_captured);
// Send success signal
dbus::Signal signal("org.freedesktop.portal.Request", "Response");
dbus::MessageWriter signal_writer(&signal);
signal_writer.AppendUint32(0); // Response success
dbus_xdg::Dictionary results;
results["token"] = dbus_utils::Variant::Wrap<"u">(12345);
dbus_utils::WriteValue(signal_writer, results);
signal_callback_captured.Run(&signal);
run_loop.Run();
}
TEST_F(PrintDialogLinuxPortalTest, ShowDialog_PortalCancelled) {
dbus_xdg::SetPortalStateForTesting(dbus_xdg::PortalRegistrarState::kSuccess);
dialog_ = std::make_unique<PrintDialogLinuxPortal>(context_.get(), mock_bus_);
dbus::ObjectProxy::ResponseOrErrorCallback prepare_print_callback;
dbus::ObjectProxy::SignalCallback signal_callback_captured;
// Expect PreparePrint call
EXPECT_CALL(*mock_proxy_, CallMethodWithErrorResponse(_, _, _))
.WillOnce([&prepare_print_callback](
dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseOrErrorCallback callback) {
prepare_print_callback = std::move(callback);
});
// Expect Response signal - cancellation (1)
EXPECT_CALL(*mock_proxy_, ConnectToSignal(_, _, _, _))
.WillRepeatedly(
[&signal_callback_captured](
const std::string& interface_name, const std::string& signal_name,
dbus::ObjectProxy::SignalCallback signal_callback,
dbus::ObjectProxy::OnConnectedCallback on_connected_callback) {
signal_callback_captured = signal_callback;
std::move(on_connected_callback)
.Run(interface_name, signal_name, true);
});
base::RunLoop run_loop;
dialog_->ShowDialog(
nullptr, false,
base::BindOnce(
[](base::RunLoop* run_loop, mojom::ResultCode result) {
EXPECT_EQ(result, mojom::ResultCode::kCanceled);
run_loop->Quit();
},
&run_loop));
ASSERT_TRUE(prepare_print_callback);
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendObjectPath(
dbus::ObjectPath("/org/freedesktop/portal/request/2"));
std::move(prepare_print_callback).Run(response.get(), nullptr);
ASSERT_TRUE(signal_callback_captured);
dbus::Signal signal("org.freedesktop.portal.Request", "Response");
dbus::MessageWriter signal_writer(&signal);
signal_writer.AppendUint32(1); // Cancelled
dbus_utils::WriteValue(signal_writer, dbus_xdg::Dictionary());
signal_callback_captured.Run(&signal);
run_loop.Run();
}
TEST_F(PrintDialogLinuxPortalTest, PrintDocument_WriteToPipe) {
dialog_ = std::make_unique<PrintDialogLinuxPortal>(context_.get(), mock_bus_);
MockMetafile metafile;
std::string_view kData = "test data";
EXPECT_CALL(metafile, GetDataAsVector(_))
.WillOnce([&kData](std::vector<char>* buffer) {
buffer->assign(kData.begin(), kData.end());
return true;
});
base::ScopedFD read_fd; // To be captured from DBus call
// Expect Print call
EXPECT_CALL(*mock_proxy_,
CallMethodWithErrorResponse(
testing::ResultOf(
[](dbus::MethodCall* call) { return call->GetMember(); },
"Print"),
_, _))
.WillOnce(
[&read_fd](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseOrErrorCallback callback) {
// Retrieve FD from message
dbus::MessageReader reader(method_call);
std::string parent_handle;
reader.PopString(&parent_handle);
std::string title;
reader.PopString(&title);
reader.PopFileDescriptor(&read_fd);
});
dialog_->PrintDocument(metafile, u"title");
// Wait for the Print DBus call to populate read_fd.
EXPECT_TRUE(base::test::RunUntil([&]() { return read_fd.is_valid(); }));
ASSERT_TRUE(read_fd.is_valid());
// Wait for data to be available in the pipe.
base::RunLoop read_loop;
auto controller = base::FileDescriptorWatcher::WatchReadable(
read_fd.get(), read_loop.QuitClosure());
read_loop.Run();
// Read from read_fd
char buffer[256];
ssize_t bytes_read =
HANDLE_EINTR(read(read_fd.get(), buffer, sizeof(buffer)));
ASSERT_GT(bytes_read, 0);
std::string_view read_data(buffer, bytes_read);
EXPECT_EQ(read_data, kData);
}
} // namespace printing