use super::*;
use crate::service_messages::DENYLIST;
use crate::test_support::{executable, fixture, remove};
fn fake_manager(root: &Path, manager: bool, enable: bool, active: bool) -> PathBuf {
let path: PathBuf = root.join("systemctl");
let exit = |ok: bool| { return if ok { 0 } else { 1 }; };
let script: String = format!(
"#!/bin/sh\necho \"$@\" >> '{}'\ncase \"$*\" in\n*show-environment*) exit {};;\n*enable*) echo 'unit file is read-only' >&2; exit {};;\n*is-active*) exit {};;\nesac\nexit 0\n",
root.join("calls.log").display(),
exit(manager),
exit(enable),
exit(active)
);
executable(path.as_path(), script.as_bytes());
return path;
}
fn host(root: &Path, manager: PathBuf) -> ServiceHost {
let binary: PathBuf = root.join("cli-git-service");
std::fs::write(&binary, b"binary").expect("service program");
return ServiceHost { unit_directory: root.join("units"), systemctl: manager, service_binary: binary, git: PathBuf::from("/usr/bin/git") };
}
fn calls(root: &Path) -> Vec<String> {
return std::fs::read_to_string(root.join("calls.log")).unwrap_or_default().lines().map(str::to_string).collect();
}
fn plain(text: &str) -> String {
return text.split('`').step_by(2).collect::<Vec<&str>>().join(" ").to_lowercase();
}
#[test]
fn an_install_writes_the_units_and_enables_the_socket() {
let root: PathBuf = fixture("service-install");
let host: ServiceHost = host(root.as_path(), fake_manager(root.as_path(), true, true, true));
let report: Report = install(&host);
assert_eq!(report.code, 0, "{report:?}");
for (name, text) in unit_texts(&host) {
assert_eq!(std::fs::read_to_string(host.unit_directory.join(name)).expect("unit written"), text);
assert!(!text.contains('@'), "no placeholder is left in {name}");
}
let service: String = std::fs::read_to_string(host.unit_directory.join(SERVICE_UNIT_NAME)).expect("service unit");
assert!(service.contains(host.service_binary.to_string_lossy().as_ref()) && service.contains("/usr/bin/git"));
let log: Vec<String> = calls(root.as_path());
assert!(log.iter().any(|call| { return call == &format!("--user enable --now {SOCKET_UNIT_NAME}"); }), "{log:?}");
assert!(!log.iter().any(|call| { return call.contains("start") && !call.contains("--now"); }), "the service is never started directly: {log:?}");
assert!(report.stdout.contains("Installed") && report.stdout.contains(SOCKET_UNIT_NAME), "{}", report.stdout);
remove(root.as_path());
}
#[test]
fn a_second_install_changes_nothing_and_says_so() {
let root: PathBuf = fixture("service-install-twice");
let host: ServiceHost = host(root.as_path(), fake_manager(root.as_path(), true, true, true));
assert_eq!(install(&host).code, 0);
let before: Vec<(&str, std::time::SystemTime)> = [SOCKET_UNIT_NAME, SERVICE_UNIT_NAME]
.map(|name| { return (name, std::fs::metadata(host.unit_directory.join(name)).expect("unit").modified().expect("time")); })
.to_vec();
let again: Report = install(&host);
assert_eq!(again.code, 0);
assert!(again.stdout.contains("already installed"), "{}", again.stdout);
for (name, time) in before {
assert_eq!(std::fs::metadata(host.unit_directory.join(name)).expect("unit").modified().expect("time"), time, "{name}");
}
remove(root.as_path());
}
#[test]
fn a_moved_program_rewrites_the_service_unit() {
let root: PathBuf = fixture("service-install-moved");
let mut host: ServiceHost = host(root.as_path(), fake_manager(root.as_path(), true, true, true));
assert_eq!(install(&host).code, 0);
let moved: PathBuf = root.join("elsewhere-service");
std::fs::write(&moved, b"binary").expect("moved");
host.service_binary = moved.clone();
let report: Report = install(&host);
assert!(report.stdout.contains(SERVICE_UNIT_NAME) && !report.stdout.contains(&format!("`{}`", host.unit_directory.join(SOCKET_UNIT_NAME).display())), "{}", report.stdout);
assert!(std::fs::read_to_string(host.unit_directory.join(SERVICE_UNIT_NAME)).expect("unit").contains(moved.to_string_lossy().as_ref()));
remove(root.as_path());
}
#[test]
fn install_failures_are_the_plain_messages() {
let root: PathBuf = fixture("service-install-failures");
let no_program: ServiceHost = ServiceHost { service_binary: root.join("absent"), ..host(root.as_path(), fake_manager(root.as_path(), true, true, true)) };
let missing: Report = install(&no_program);
assert_eq!(missing.code, 1);
assert_eq!(missing.stderr, format!("{}\n", render(&ClientFailure::InstallProgramMissing(root.join("absent").display().to_string()))));
assert!(!no_program.unit_directory.exists(), "nothing was written");
let no_manager: ServiceHost = host(root.as_path(), fake_manager(root.as_path(), false, true, true));
let unavailable: Report = install(&no_manager);
assert_eq!(unavailable.stderr, format!("{}\n", render(&ClientFailure::NoActivation(platform_without_manager()))));
let refusing: ServiceHost = host(root.as_path(), fake_manager(root.as_path(), true, false, true));
let refused: Report = install(&refusing);
assert_eq!(refused.code, 1);
assert!(refused.stderr.contains("unit file is read-only") && refused.stderr.contains(INSTALL_COMMAND_FOR_TEST), "{}", refused.stderr);
let absent_control: ServiceHost = ServiceHost { systemctl: root.join("no-such-program"), ..no_manager };
assert_eq!(install(&absent_control).stderr, unavailable.stderr, "no control program is the same as no manager");
remove(root.as_path());
}
const INSTALL_COMMAND_FOR_TEST: &str = crate::service_messages::INSTALL_COMMAND;
#[test]
fn an_uninstall_removes_the_units_and_a_second_one_says_nothing_was_there() {
let root: PathBuf = fixture("service-uninstall");
let host: ServiceHost = host(root.as_path(), fake_manager(root.as_path(), true, true, true));
assert_eq!(install(&host).code, 0);
let removed: Report = uninstall(&host);
assert_eq!(removed.code, 0);
assert!(removed.stdout.contains("Removed"), "{}", removed.stdout);
for name in [SOCKET_UNIT_NAME, SERVICE_UNIT_NAME] {
assert!(!host.unit_directory.join(name).exists(), "{name}");
}
assert!(calls(root.as_path()).iter().any(|call| { return call.contains("disable --now"); }));
let nothing: Report = uninstall(&host);
assert_eq!(nothing.code, 0);
assert!(nothing.stdout.contains("was not installed"), "{}", nothing.stdout);
remove(root.as_path());
}
#[test]
fn status_follows_the_socket() {
let root: PathBuf = fixture("service-status");
let listening: Report = status(&host(root.as_path(), fake_manager(root.as_path(), true, true, true)));
assert_eq!(listening.code, 0);
let silent: Report = status(&host(root.as_path(), fake_manager(root.as_path(), true, true, false)));
assert_eq!(silent.code, 1);
assert_eq!(silent.stderr, format!("{}\n", render(&ClientFailure::NotInstalled)));
remove(root.as_path());
}
#[test]
fn reports_leak_no_implementation_term() {
let root: PathBuf = fixture("service-install-words");
let host: ServiceHost = host(root.as_path(), fake_manager(root.as_path(), true, true, true));
let texts: Vec<String> = vec![install(&host).stdout, install(&host).stdout, uninstall(&host).stdout, uninstall(&host).stdout];
for text in texts {
let words: String = plain(text.as_str());
for term in DENYLIST {
let at: Option<usize> = words.find(term);
let whole: bool = at.is_some_and(|start| {
let before: bool = words[..start].chars().next_back().is_none_or(|c| { return !c.is_alphanumeric(); });
let after: bool = words[start + term.len()..].chars().next().is_none_or(|c| { return !c.is_alphanumeric(); });
return before && after;
});
assert!(!whole, "{term:?} in {words:?}");
}
}
remove(root.as_path());
}
#[test]
fn the_unit_directory_follows_the_configuration_home() {
assert_eq!(unit_directory(Some(Path::new("/cfg")), Some(Path::new("/home/a"))), Some(PathBuf::from("/cfg/systemd/user")));
assert_eq!(unit_directory(None, Some(Path::new("/home/a"))), Some(PathBuf::from("/home/a/.config/systemd/user")));
assert_eq!(unit_directory(None, None), None);
}