crates/gunnar-ui-server/tests/generated_unit.rs
//! **The generated unit, against the hand-written one it replaces — field by
//! field.**
//!
//! A test that compared the generator with its own output would measure
//! nothing. [`HANDWRITTEN`] below is `deploy/gunnar-ui.service` transcribed
//! verbatim at commit `94eaa98`, the last commit in which that file existed; it
//! is deleted in the commit that adds this test, and this constant is what it
//! left behind. Every directive in it must appear, with the same value, in what
//! `gunnar_ui_server::lifecycle` renders — and every directive the generated
//! unit has that the hand-written one did not is listed in
//! [`the_only_additions`], so nothing can arrive quietly.
use std::path::Path;
use gunnar_ui_server::lifecycle::{self, Settings};
use skidbladnir::systemd::Scope;
/// `deploy/gunnar-ui.service` at 94eaa98, comments stripped (a comment is not a
/// directive). This is the thing under test, not a copy of the answer.
const HANDWRITTEN: &str = "\
[Unit]
Description=gunnar console (BFF + SPA)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/gunnar-ui/current
EnvironmentFile=/etc/gunnar-ui.env
ExecStart=/opt/gunnar-ui/current/gunnar-ui-server
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
";
/// `(section, key, value)` for every directive line, in file order.
fn directives(unit: &str) -> Vec<(String, String, String)> {
let mut section = String::new();
let mut out = Vec::new();
for line in unit.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
section = name.to_string();
continue;
}
if let Some((k, v)) = line.split_once('=') {
out.push((section.clone(), k.trim().to_string(), v.trim().to_string()));
}
}
out
}
fn generated() -> String {
lifecycle::unit_text(
Path::new(lifecycle::EXEC),
&Settings::default(),
Scope::System,
&[],
)
}
#[test]
fn every_field_the_handwritten_unit_had_is_in_the_generated_one() {
let want = directives(HANDWRITTEN);
let got = directives(&generated());
assert!(!want.is_empty() && !got.is_empty());
let mut missing = Vec::new();
let mut changed = Vec::new();
for (section, key, value) in &want {
match got.iter().find(|(s, k, _)| s == section && k == key) {
None => missing.push(format!("[{section}] {key}={value}")),
Some((_, _, got_value)) if got_value != value => changed.push(format!(
"[{section}] {key}: was {value:?}, now {got_value:?}"
)),
Some(_) => {}
}
}
assert!(
missing.is_empty() && changed.is_empty(),
"the generated unit does not cover deploy/gunnar-ui.service.\n missing: {missing:?}\n \
changed: {changed:?}\n\ngenerated:\n{}",
generated()
);
}
#[test]
fn the_only_additions_are_the_account_and_the_hardening_line() {
// The hand-written unit named no `User=`, which in a system unit means root,
// and root is what the console on the front has been running as. skidbladnir
// always writes the account down in system scope, so it is now stated rather
// than implied — the same account, spelled out.
//
// `NoNewPrivileges=true` is skidbladnir's default and the one genuine
// behaviour change. It is safe here because this process spawns nothing: it
// has no `Command::new` anywhere in `src/`, so there is no setuid binary for
// it to lose the ability to elevate through.
let want = directives(HANDWRITTEN);
let extra: Vec<String> = directives(&generated())
.into_iter()
.filter(|(s, k, _)| !want.iter().any(|(ws, wk, _)| ws == s && wk == k))
.map(|(s, k, v)| format!("[{s}] {k}={v}"))
.collect();
assert_eq!(
extra,
vec![
"[Service] User=root".to_string(),
"[Service] Group=root".to_string(),
"[Service] NoNewPrivileges=true".to_string(),
],
"something new appeared in the generated unit. Every directive here changes what runs \
on the front; add it to this list deliberately or not at all.\n\ngenerated:\n{}",
generated()
);
}
#[test]
fn nothing_configurable_is_inline_in_the_world_readable_unit() {
// The hand-written unit had no `Environment=` line and its comment said why:
// systemd applies `EnvironmentFile=` after `Environment=` whatever order
// they appear in, so a value in the unit is ignored and the file always
// wins. A generated unit that started emitting them would be writing lines
// that do nothing — and, for `GUNNAR_UI_ADMIN_TOKEN`, writing a credential
// into a mode-0644 file.
let unit = lifecycle::unit_text(
Path::new(lifecycle::EXEC),
&Settings::default(),
Scope::System,
&[("GUNNAR_UI_ADMIN_TOKEN".into(), "not-in-the-unit".into())],
);
assert!(!unit.contains("Environment="), "{unit}");
assert!(!unit.contains("not-in-the-unit"), "{unit}");
assert!(
unit.contains("EnvironmentFile=/etc/gunnar-ui.env"),
"{unit}"
);
}