blob: fbbb174a7b1ba3856f3cbcf1468337bca8c759cb [file] [log] [blame]
// Copyright 2024 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/dbus/xdg/systemd.h"
#include <optional>
#include "base/environment.h"
#include "base/memory/scoped_refptr.h"
#include "base/scoped_environment_variable_override.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "components/dbus/xdg/request.h"
#include "dbus/message.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "dbus/property.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::Return;
namespace dbus_xdg {
namespace {
constexpr char kServiceNameSystemd[] = "org.freedesktop.systemd1";
constexpr char kObjectPathSystemd[] = "/org/freedesktop/systemd1";
constexpr char kInterfaceSystemdManager[] = "org.freedesktop.systemd1.Manager";
constexpr char kMethodStartTransientUnit[] = "StartTransientUnit";
constexpr char kMethodGetUnit[] = "GetUnit";
constexpr char kFakeUnitPath[] = "/fake/unit/path";
constexpr char kActiveState[] = "ActiveState";
constexpr char kStateActive[] = "active";
constexpr char kStateInactive[] = "inactive";
std::unique_ptr<dbus::Response> CreateActiveStateGetAllResponse(
const std::string& state) {
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
dbus::MessageWriter array_writer(nullptr);
dbus::MessageWriter dict_entry_writer(nullptr);
writer.OpenArray("{sv}", &array_writer);
array_writer.OpenDictEntry(&dict_entry_writer);
dict_entry_writer.AppendString(kActiveState);
dict_entry_writer.AppendVariantOfString(state);
array_writer.CloseContainer(&dict_entry_writer);
writer.CloseContainer(&array_writer);
return response;
}
class SetSystemdScopeUnitNameForXdgPortalTest : public ::testing::Test {
public:
void SetUp() override { ResetCachedStateForTesting(); }
void TearDown() override { ResetCachedStateForTesting(); }
};
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, NotNecessaryInFlatpak) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
base::ScopedEnvironmentVariableOverride env_override("FLATPAK_SANDBOX_DIR",
"/");
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kUnitNotNecessary);
}
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, NotNecessaryInSnap) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
base::ScopedEnvironmentVariableOverride env_override("SNAP", "/");
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kUnitNotNecessary);
}
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, NoSystemdService) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
EXPECT_CALL(
*bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy.get()));
EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(false);
std::move(callback).Run(response.get());
});
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kNoSystemdService);
}
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitSuccess) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
EXPECT_CALL(
*bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy.get()));
EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
std::move(callback).Run(response.get());
});
auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kObjectPathSystemd)))
.Times(2)
.WillRepeatedly(Return(mock_systemd_proxy.get()));
auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kFakeUnitPath)))
.WillOnce(Return(mock_dbus_unit_proxy.get()));
EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
// Expect kMethodStartTransientUnit first.
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
// Simulate a successful response
auto response = dbus::Response::CreateEmpty();
std::move(callback).Run(response.get());
})
.WillOnce([obj_path = kFakeUnitPath](
dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
// Then expect kMethodGetUnit. A valid path must be provided.
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
// Simulate a successful response and provide a fake path to the object.
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendObjectPath(dbus::ObjectPath(obj_path));
std::move(callback).Run(response.get());
});
EXPECT_CALL(*mock_dbus_unit_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
// Simulate a successful response with "active" state.
auto response = CreateActiveStateGetAllResponse(kStateActive);
std::move(callback).Run(response.get());
});
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kUnitStarted);
}
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitFailure) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
EXPECT_CALL(
*bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy.get()));
EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
std::move(callback).Run(response.get());
});
auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kObjectPathSystemd)))
.WillOnce(Return(mock_systemd_proxy.get()));
EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
// Simulate a failure by invoking the callback with nullptr
std::move(callback).Run(nullptr);
});
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
}
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
StartTransientUnitInvalidUnitPath) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
EXPECT_CALL(
*bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy.get()));
EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
std::move(callback).Run(response.get());
});
auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kObjectPathSystemd)))
.Times(2)
.WillRepeatedly(Return(mock_systemd_proxy.get()));
EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
// Simulate a successful response
auto response = dbus::Response::CreateEmpty();
std::move(callback).Run(response.get());
})
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
// Simulate a failure response.
std::move(callback).Run(nullptr);
});
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
}
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
StartTransientUnitFailToActivate) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
EXPECT_CALL(
*bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy.get()));
EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
std::move(callback).Run(response.get());
});
auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kObjectPathSystemd)))
.Times(2)
.WillRepeatedly(Return(mock_systemd_proxy.get()));
auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kFakeUnitPath)))
.WillOnce(Return(mock_dbus_unit_proxy.get()));
EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);
// Simulate a successful response
auto response = dbus::Response::CreateEmpty();
std::move(callback).Run(response.get());
})
.WillOnce([obj_path = kFakeUnitPath](
dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
// Simulate a successful response
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendObjectPath(dbus::ObjectPath(obj_path));
std::move(callback).Run(response.get());
});
EXPECT_CALL(*mock_dbus_unit_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
// Then expect kMethodGetUnit. A valid path must be provided.
EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
// Simulate a successful response, but with inactive state.
auto response = CreateActiveStateGetAllResponse(kStateInactive);
std::move(callback).Run(response.get());
});
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
}
TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
scoped_refptr<dbus::MockBus> bus =
base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
EXPECT_CALL(
*bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy.get()));
EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
std::move(callback).Run(response.get());
});
base::ScopedEnvironmentVariableOverride env_override("CHROME_VERSION_EXTRA",
"beta");
auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));
constexpr std::string_view kAppPrefix = "app-";
constexpr std::string_view kScopeSuffix = ".scope";
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kObjectPathSystemd)))
.Times(2)
.WillRepeatedly(Return(mock_systemd_proxy.get()));
auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
dbus::ObjectPath(kFakeUnitPath)))
.WillOnce(Return(mock_dbus_unit_proxy.get()));
EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
.WillOnce([&](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
dbus::MessageReader reader(method_call);
std::string unit_name;
EXPECT_TRUE(reader.PopString(&unit_name));
// Check that the unit_name matches the expected format
// Format: app-<ApplicationID>-<RANDOM>.scope
EXPECT_TRUE(base::StartsWith(unit_name, kAppPrefix));
EXPECT_TRUE(base::EndsWith(unit_name, kScopeSuffix));
// Remove "app-" prefix and ".scope" suffix
std::string name_body = unit_name.substr(
kAppPrefix.size(),
unit_name.size() - kAppPrefix.size() - kScopeSuffix.size());
auto id_random = base::SplitString(
name_body, "-", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
ASSERT_EQ(id_random.size(), 2U);
std::string application_id = id_random[0];
EXPECT_FALSE(application_id.empty());
EXPECT_TRUE(
std::all_of(application_id.begin(), application_id.end(),
[](char c) { return std::isalnum(c) || c == '.'; }));
std::string random_part = id_random[1];
EXPECT_FALSE(random_part.empty());
EXPECT_TRUE(std::all_of(random_part.begin(), random_part.end(),
[](char c) { return std::isalnum(c); }));
auto response = dbus::Response::CreateEmpty();
std::move(callback).Run(response.get());
})
.WillOnce([obj_path = kFakeUnitPath](
dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);
// Simulate a successful response
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendObjectPath(dbus::ObjectPath(obj_path));
std::move(callback).Run(response.get());
});
EXPECT_CALL(*mock_dbus_unit_proxy, CallMethod(_, _, _))
.WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback callback) {
// Then expect kMethodGetUnit. A valid path must be provided.
EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
// Simulate a successful response
auto response = CreateActiveStateGetAllResponse(kStateActive);
std::move(callback).Run(response.get());
});
std::optional<SystemdUnitStatus> status;
SetSystemdScopeUnitNameForXdgPortal(
bus.get(), base::BindLambdaForTesting(
[&](SystemdUnitStatus result) { status = result; }));
EXPECT_EQ(status, SystemdUnitStatus::kUnitStarted);
}
} // namespace
} // namespace dbus_xdg