1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//! The nornir data root is derived from the **running user's home** — ONE rule
//! for everyone, no path env-vars. This test proves it two ways:
//!
//! 1. the in-process resolver returns `$HOME/.nornir` for whoever runs the test;
//! 2. (gated on `sudo -n -H -u nornir true` succeeding) the built `nornir root`
//! binary, run *as the server's system user* via `sudo -H -u nornir`, prints
//! `/home/nornir/.nornir` — identical home-derived behavior for "me or
//! nornir", which is the whole point of the refactor.
use std::path::PathBuf;
use std::process::Command;
#[test]
fn root_is_home_derived_and_same_for_any_user() {
// --- (1) In-process: the running user's resolution is exactly $HOME/.nornir.
let home = std::env::var("HOME").expect("HOME set for the test runner");
let expected = PathBuf::from(&home).join(".nornir");
assert_eq!(
nornir::config::nornir_home(),
expected,
"nornir_home() must be $HOME/.nornir for the running user",
);
// The binary agrees with the library when run as the current user.
let out = Command::new(env!("CARGO_BIN_EXE_nornir"))
.arg("root")
.output()
.expect("run `nornir root`");
assert!(out.status.success(), "`nornir root` exited non-zero");
let printed = String::from_utf8(out.stdout).unwrap();
assert_eq!(
printed.trim(),
expected.display().to_string(),
"`nornir root` must print the running user's $HOME/.nornir",
);
// --- (2) Cross-user proof: same code, run as the `nornir` service user via
// `sudo -H -u nornir`, must derive THAT user's home → /home/nornir/.nornir.
// Gated on passwordless sudo to the nornir user being available; otherwise
// skipped with a logged note (so CI without sudo still passes).
let sudo_ok = Command::new("sudo")
.args(["-n", "-H", "-u", "nornir", "true"])
.status()
.map(|s| s.success())
.unwrap_or(false);
if !sudo_ok {
eprintln!(
"note: skipping the `sudo -H -u nornir nornir root` cross-user check \
— passwordless `sudo -n -H -u nornir` is not available here"
);
return;
}
// The build output lives under the test runner's home (here a removable
// drive) which the `nornir` user can't traverse/execute. Stage the binary
// into a world-executable location (`/tmp`) so this proves the home RULE,
// not filesystem permissions. If staging or the run fails for an
// environment reason, skip with a note rather than fail.
let bin = env!("CARGO_BIN_EXE_nornir");
let staged = std::env::temp_dir().join("nornir-root-test-bin");
if std::fs::copy(bin, &staged).is_err() {
eprintln!("note: skipping cross-user check — could not stage the binary into /tmp");
return;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&staged, std::fs::Permissions::from_mode(0o755));
}
let out = Command::new("sudo")
.args(["-n", "-H", "-u", "nornir"])
.arg(&staged)
.arg("root")
.output()
.expect("run `sudo -H -u nornir <staged nornir> root`");
let _ = std::fs::remove_file(&staged);
assert!(
out.status.success(),
"`sudo -H -u nornir nornir root` exited non-zero: {}",
String::from_utf8_lossy(&out.stderr),
);
let printed = String::from_utf8(out.stdout).unwrap();
assert_eq!(
printed.trim(),
"/home/nornir/.nornir",
"running as the server user must derive /home/nornir/.nornir — \
identical home-derived rule, no path env-var",
);
}