You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libcutefish/clients/accounts/tests/accounts_test.cpp

315 lines
12 KiB
C++

// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
// SPDX-License-Identifier: BSD-3-Clause
#include <gtest/gtest.h>
#include <chrono>
#include <future>
#include <map>
#include <memory>
#include <thread>
#include "base/event_loop.h"
#include "cutefish/accounts/accounts_manager.h"
#include "cutefish/accounts/user_account.h"
using namespace cutefish;
using namespace cutefish::accounts;
using namespace cutefish::base;
using namespace cutefish::base::dbus;
using namespace std::chrono_literals;
namespace {
constexpr const char* kService = "org.freedesktop.Accounts";
constexpr const char* kUserInterface = "org.freedesktop.Accounts.User";
// org.freedesktop.Accounts on the session bus, on a thread of its own.
class FakeAccountsService
{
public:
FakeAccountsService()
{
std::promise<bool> ready;
m_thread = std::thread([this, &ready] { run(ready); });
m_ready = ready.get_future().get();
}
~FakeAccountsService()
{
m_handle.post([this] { m_loop->quit(); });
m_thread.join();
}
bool isReady() const { return m_ready; }
// Read on the service's thread.
template<typename F>
auto inService(F f)
{
std::promise<decltype(f())> result;
m_handle.post([&] {
if constexpr (std::is_void_v<decltype(f())>) {
f();
result.set_value();
} else {
result.set_value(f());
}
});
return result.get_future().get();
}
struct User
{
std::string name;
std::string realName;
int32_t accountType = 0;
std::string password;
std::unique_ptr<Object> object;
};
std::map<int64_t, User> users;
bool failSetRealName = false;
bool createAskedInteractively = false;
bool setPasswordAskedInteractively = false;
private:
static std::string path(int64_t uid) { return "/org/freedesktop/Accounts/User" + std::to_string(uid); }
void addUser(int64_t uid, std::string name, std::string realName)
{
User& user = users[uid];
user.name = std::move(name);
user.realName = std::move(realName);
user.object = std::make_unique<Object>(m_connection, path(uid), kUserInterface);
Object& object = *user.object;
object.addProperty("Uid", [uid] { return uint64_t(uid); });
object.addProperty("UserName", [this, uid] { return users[uid].name; });
object.addProperty("RealName", [this, uid] { return users[uid].realName; });
object.addProperty("AccountType", [this, uid] { return users[uid].accountType; });
object.addProperty("IconFile", [] { return std::string("/nonexistent/icon"); });
object.addMethod("SetRealName", "s", "", [this, uid](MethodCall& call) {
std::string name;
call.read(name);
// Answers a little later, as a real service waiting for polkit does.
m_loop->startTimer(30ms, false, [this, uid, call, name]() mutable {
if (failSetRealName) {
call.replyError("org.freedesktop.Accounts.Error.PermissionDenied", "Not allowed");
return;
}
users[uid].realName = name;
users[uid].object->emitSignal("Changed");
call.reply();
});
});
object.addMethod("SetPassword", "ss", "", [this, uid](MethodCall& call) {
std::string password, hint;
call.read(password, hint);
users[uid].password = password;
setPasswordAskedInteractively = call.message().allowsInteractiveAuthorization();
call.reply();
});
object.addSignal("Changed", "");
object.publish();
}
void run(std::promise<bool>& ready)
{
EventLoop loop;
m_loop = &loop;
m_handle = loop.handle();
std::unique_ptr<Connection> connection = Connection::open(Connection::Bus::Session, &loop);
if (!connection || !connection->requestName(kService)) {
ready.set_value(false);
return;
}
m_connection = connection.get();
addUser(1000, "reion", "Reion Wong");
addUser(1001, "guest", "");
Object manager(m_connection, "/org/freedesktop/Accounts", "org.freedesktop.Accounts");
manager.addMethod("ListCachedUsers", [this] {
std::vector<ObjectPath> paths;
for (const auto& [uid, user] : users)
paths.push_back({path(uid)});
return paths;
});
manager.addMethod("FindUserById", "x", "o", [this](MethodCall& call) {
int64_t uid = 0;
call.read(uid);
if (!users.contains(uid))
call.replyError("org.freedesktop.Accounts.Error.Failed", "No such user");
else
call.reply(ObjectPath{path(uid)});
});
manager.addMethod("CreateUser", "ssi", "o", [this, &manager](MethodCall& call) {
std::string name, realName;
int32_t type = 0;
call.read(name, realName, type);
createAskedInteractively = call.message().allowsInteractiveAuthorization();
if (name == "taken") {
call.replyError("org.freedesktop.Accounts.Error.Failed", "A user with name 'taken' already exists");
return;
}
const int64_t uid = 1000 + int64_t(users.size());
addUser(uid, name, realName);
users[uid].accountType = type;
manager.emitSignal("UserAdded", ObjectPath{path(uid)});
call.reply(ObjectPath{path(uid)});
});
manager.addMethod("DeleteUser", "xb", "", [this, &manager](MethodCall& call) {
int64_t uid = 0;
bool removeFiles = false;
call.read(uid, removeFiles);
users.erase(uid);
manager.emitSignal("UserDeleted", ObjectPath{path(uid)});
call.reply();
});
manager.addSignal("UserAdded", "o");
manager.addSignal("UserDeleted", "o");
ready.set_value(manager.publish());
loop.run();
users.clear();
m_connection = nullptr;
}
std::thread m_thread;
EventLoop* m_loop = nullptr;
LoopHandle m_handle;
Connection* m_connection = nullptr;
bool m_ready = false;
};
class AccountsTest : public testing::Test
{
protected:
void SetUp() override
{
if (!Connection::session(&m_loop))
GTEST_SKIP() << "No session bus; run the tests in dbus-run-session";
m_service = std::make_unique<FakeAccountsService>();
ASSERT_TRUE(m_service->isReady());
}
// Runs the loop until the condition holds or two seconds pass.
bool waitFor(const std::function<bool()>& condition)
{
const auto deadline = std::chrono::steady_clock::now() + 2s;
while (!condition()) {
if (std::chrono::steady_clock::now() > deadline)
return false;
runFor(5ms);
}
return true;
}
void runFor(std::chrono::milliseconds time)
{
const int timer = m_loop.startTimer(time, false, [this] { m_loop.quit(); });
m_loop.run();
m_loop.stopTimer(timer);
}
Connection* bus() { return Connection::session(&m_loop); }
EventLoop m_loop;
std::unique_ptr<FakeAccountsService> m_service;
};
} // namespace
TEST_F(AccountsTest, ListsAccountsAndReadsTheirValues)
{
AccountsManager manager(bus());
int changes = 0;
Subscription subscription = manager.accountsChanged.connect([&] { ++changes; });
ASSERT_TRUE(waitFor([&] { return manager.isLoaded(); }));
EXPECT_EQ(changes, 1);
ASSERT_EQ(manager.accounts().size(), 2u);
const auto& reion = manager.accounts()[0];
// Known from the path at once; the rest once the read answers.
EXPECT_EQ(reion->userId(), 1000);
ASSERT_TRUE(waitFor([&] { return reion->userName() == "reion"; }));
EXPECT_EQ(reion->displayName(), "Reion Wong");
const auto& guest = manager.accounts()[1];
ASSERT_TRUE(waitFor([&] { return guest->userName() == "guest"; }));
EXPECT_EQ(guest->displayName(), "guest");
// A picture that is not there is no picture.
EXPECT_EQ(guest->iconFileName(), "");
EXPECT_EQ(manager.findUserById(1001), guest);
EXPECT_FALSE(manager.findUserById(4242));
}
TEST_F(AccountsTest, ASetValueShowsUntilTheServiceAnswers)
{
UserAccount account(1000, bus());
ASSERT_TRUE(waitFor([&] { return account.realName() == "Reion Wong"; }));
int changes = 0;
Subscription subscription = account.changed.connect([&] { ++changes; });
account.setRealName("Reion");
EXPECT_EQ(account.realName(), "Reion");
EXPECT_EQ(changes, 1);
// The service's Changed re-reads, and the reply ends the pending value.
ASSERT_TRUE(waitFor([&] { return m_service->inService([&] { return m_service->users[1000].realName; }) == "Reion"; }));
runFor(100ms);
EXPECT_EQ(account.realName(), "Reion");
}
TEST_F(AccountsTest, ARefusedValueGoesBack)
{
m_service->inService([&] { m_service->failSetRealName = true; });
UserAccount account(1000, bus());
ASSERT_TRUE(waitFor([&] { return account.realName() == "Reion Wong"; }));
account.setRealName("Someone");
EXPECT_EQ(account.realName(), "Someone");
EXPECT_TRUE(waitFor([&] { return account.realName() == "Reion Wong"; }));
}
TEST_F(AccountsTest, CreatesAndDeletesAccounts)
{
AccountsManager manager(bus());
ASSERT_TRUE(waitFor([&] { return manager.isLoaded(); }));
int64_t deleted = -1;
Subscription subscription = manager.userDeleted.connect([&](int64_t uid) { deleted = uid; });
AccountsManager::AccountPtr created;
std::string error = "unset";
manager.createUser("alice", "", UserAccount::Administrator,
[&](const AccountsManager::AccountPtr& account, const std::string& message) {
created = account;
error = message;
});
ASSERT_TRUE(waitFor([&] { return error != "unset"; }));
EXPECT_EQ(error, "");
ASSERT_TRUE(created);
EXPECT_TRUE(m_service->inService([&] { return m_service->createAskedInteractively; }));
EXPECT_EQ(manager.accounts().size(), 3u);
EXPECT_EQ(manager.accounts().back(), created);
ASSERT_TRUE(waitFor([&] { return created->userName() == "alice"; }));
EXPECT_EQ(created->accountType(), UserAccount::Administrator);
created->setPassword("$6$salt$hash");
ASSERT_TRUE(waitFor([&] { return m_service->inService([&] { return m_service->users[1002].password; }) == "$6$salt$hash"; }));
EXPECT_TRUE(m_service->inService([&] { return m_service->setPasswordAskedInteractively; }));
std::string deleteError = "unset";
manager.deleteUser(created->userId(), true, [&](const std::string& message) { deleteError = message; });
ASSERT_TRUE(waitFor([&] { return deleteError != "unset" && deleted >= 0; }));
EXPECT_EQ(deleteError, "");
EXPECT_EQ(deleted, 1002);
EXPECT_EQ(manager.accounts().size(), 2u);
}
TEST_F(AccountsTest, AFailedCreationReportsTheServicesMessage)
{
AccountsManager manager(bus());
std::string error;
bool done = false;
manager.createUser("taken", "", UserAccount::Standard,
[&](const AccountsManager::AccountPtr& account, const std::string& message) {
EXPECT_FALSE(account);
error = message;
done = true;
});
ASSERT_TRUE(waitFor([&] { return done; }));
EXPECT_EQ(error, "A user with name 'taken' already exists");
}