Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

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}"
    );
}