yams/tests/unit/daemon/systemd_user_unit_catch2_test.cpp
trvon a8f891879f
feat(cli): drive the packaged systemd user unit when it is installed
With the daemon shipped as a systemd user unit, the CLI must not fight
systemd for it:

- Auto-start (DaemonClient::startDaemon): when the unit is installed but
  not running and would run exactly the daemon the client expects (same
  socket, data dir and config as the default resolution, default
  binary), start it with `systemctl --user start` instead of forking a
  copy that a later login would race for the socket. Flag or
  environment overrides (--socket, --data-dir, YAMS_DAEMON_SOCKET,
  YAMS_DATA_DIR, YAMS_CONFIG, YAMS_DAEMON_BIN) keep the fork path.
- `yams daemon start|stop|restart` use `systemctl --user` for an
  installed/active unit (start/restart without custom path options,
  stop/restart without --socket), falling back to IPC if systemctl
  fails.
- `yams daemon install --user` with no options enables the packaged unit
  (`systemctl --user enable --now`) instead of writing a copy into
  ~/.config/systemd/user that would shadow it; with options it still
  writes that override and says so. `uninstall --user` reloads the
  manager after removing an override and explains how to mask the
  globally enabled packaged unit for one account.

The decision is a pure function (userUnitMatchesPaths) with Catch2
coverage; systemctl is only run with whitelisted verbs and only when a
user manager is reachable ($XDG_RUNTIME_DIR set, Linux).

Signed-off-by: trvon <me@trevon.dev>
2026-10-02 07:55:16 -06:00

60 lines
2.4 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// When may a client hand daemon start-up to the packaged systemd user unit?
#include <catch2/catch_test_macros.hpp>
#include <yams/daemon/client/systemd_user_unit.h>
namespace {
using yams::config::ResolvedRuntimePaths;
using yams::config::RuntimePathSource;
ResolvedRuntimePaths defaultPaths() {
ResolvedRuntimePaths p;
p.configFile = {"/home/u/.config/yams/config.toml", RuntimePathSource::PlatformDefault, ""};
p.dataDir = {"/home/u/.local/share/yams", RuntimePathSource::PlatformDefault, ""};
p.runtimeDir = {"/run/user/1000/yams", RuntimePathSource::PlatformDefault, ""};
p.socketPath = {"/run/user/1000/yams-daemon.sock", RuntimePathSource::PlatformDefault, ""};
return p;
}
} // namespace
TEST_CASE("user unit: default paths match the unit", "[daemon][systemd-user-unit]") {
CHECK(yams::daemon::client::userUnitMatchesPaths(defaultPaths()));
}
TEST_CASE("user unit: paths from the shared config file still match",
"[daemon][systemd-user-unit]") {
// The unit reads the same ~/.config/yams/config.toml, e.g. data_dir from `yams init`.
auto p = defaultPaths();
p.dataDir.source = RuntimePathSource::ConfigFile;
p.socketPath.source = RuntimePathSource::ConfigFile;
CHECK(yams::daemon::client::userUnitMatchesPaths(p));
}
TEST_CASE("user unit: flag or environment overrides do not match", "[daemon][systemd-user-unit]") {
for (auto source : {RuntimePathSource::Explicit, RuntimePathSource::Environment}) {
auto data = defaultPaths();
data.dataDir.source = source;
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(data));
auto socket = defaultPaths();
socket.socketPath.source = source;
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(socket));
auto config = defaultPaths();
config.configFile.source = source;
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(config));
}
auto runtime = defaultPaths();
runtime.runtimeDir.source = RuntimePathSource::Explicit;
CHECK_FALSE(yams::daemon::client::userUnitMatchesPaths(runtime));
}
TEST_CASE("user unit: only whitelisted systemctl verbs run", "[daemon][systemd-user-unit]") {
// Rejected before any process is spawned, on every platform.
CHECK_FALSE(yams::daemon::client::runUserUnit("kill"));
CHECK_FALSE(yams::daemon::client::runUserUnit("start; rm -rf /"));
}