use super::*;
use crate::style;
#[cfg(not(windows))]
#[test]
fn the_mkdir_hint_survives_a_path_a_shell_would_split() {
let refusal = HomeRefusal::Missing(PathBuf::from("/tmp/my shep home"));
let text = refusal.to_string();
assert!(
text.contains("mkdir -p '/tmp/my shep home'"),
"the remedy must name one word: {text}"
);
assert!(text.contains("no flock at /tmp/my shep home"), "{text}");
}
#[test]
fn the_missing_home_refusal_reads_exactly_this() {
let text = HomeRefusal::Missing(PathBuf::from("/srv/api")).to_string();
let expected = if cfg!(windows) {
"no flock at /srv/api\n\
did you mean to drop --home/%SHEP_HOME%? the default is %USERPROFILE%\\.shep\n\
to set up a flock there deliberately: mkdir \"/srv/api\""
} else {
"no flock at /srv/api\n\
did you mean to drop --home/$SHEP_HOME? the default is ~/.shep\n\
to set up a flock there deliberately: mkdir -p '/srv/api'"
};
assert_eq!(text, expected);
}
#[test]
fn a_newline_in_a_missing_home_cannot_forge_a_line() {
let hostile = PathBuf::from("/tmp/forge-a\nnotice[ok]: your flock is fine");
let text = HomeRefusal::Missing(hostile).to_string();
assert_eq!(
text.lines().count(),
3,
"the refusal has three lines of its own: {text:?}"
);
assert!(
!text.lines().any(|line| line.starts_with("notice[")),
"a line was forged: {text:?}"
);
assert!(
text.starts_with("no flock at /tmp/forge-a notice[ok]: your flock is fine\n"),
"the newline must become a space, not vanish: {text:?}"
);
}
#[test]
fn a_newline_in_a_relative_home_cannot_forge_a_line() {
let refusal = HomeRefusal::Relative {
knob: "--home/$SHEP_HOME",
given: PathBuf::from("forge-b\nerror[internal]: shepherd compromised"),
absolute: Some(PathBuf::from(
"/w/forge-b\nerror[internal]: shepherd compromised",
)),
};
let text = refusal.to_string();
assert_eq!(text.lines().count(), 3, "{text:?}");
assert!(
!text.lines().any(|line| line.starts_with("error[")),
"a line was forged: {text:?}"
);
}
#[test]
fn neither_half_of_an_io_refusal_can_forge_a_line() {
let refusal = HomeRefusal::Io {
path: PathBuf::from("/tmp/forge-c\nnotice[ok]: created"),
source: std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"denied\nnotice[ok]: retried and it worked",
),
};
let text = refusal.to_string();
assert_eq!(text.lines().count(), 1, "{text:?}");
assert!(!text.contains('\n'), "{text:?}");
}
#[cfg(not(windows))]
#[test]
fn an_apostrophe_in_a_path_cannot_escape_the_mkdir_hint() {
let refusal = HomeRefusal::Missing(PathBuf::from("/tmp/rin's flock"));
let text = refusal.to_string();
assert!(
text.contains(r"mkdir -p '/tmp/rin'\''s flock'"),
"an embedded quote must close and reopen: {text}"
);
}
#[cfg(windows)]
#[test]
fn the_missing_home_remedy_is_one_a_windows_shell_can_run() {
let spaced = HomeRefusal::Missing(PathBuf::from(r"C:\tmp\my shep home")).to_string();
assert_eq!(
spaced,
"no flock at C:\\tmp\\my shep home\n\
did you mean to drop --home/%SHEP_HOME%? the default is %USERPROFILE%\\.shep\n\
to set up a flock there deliberately: mkdir \"C:\\tmp\\my shep home\""
);
let quoted = HomeRefusal::Missing(PathBuf::from(r"C:\tmp\rin's flock")).to_string();
assert_eq!(
quoted,
"no flock at C:\\tmp\\rin's flock\n\
did you mean to drop --home/%SHEP_HOME%? the default is %USERPROFILE%\\.shep\n\
to set up a flock there deliberately: mkdir \"C:\\tmp\\rin's flock\""
);
}
#[cfg(unix)]
fn paths_at(root: &std::path::Path) -> ShepPaths {
let home = root.join(".shep").to_string_lossy().into_owned();
let env = |key: &str| (key == "SHEP_HOME").then(|| home.clone());
ShepPaths::resolve(&env, std::path::Path::new("/nonexistent"))
}
#[cfg(unix)]
#[test]
fn a_missing_default_home_is_created_and_reported_as_new() {
let root = tempfile::tempdir().unwrap();
let (paths, created) =
ensure_home_at(paths_at(root.path()), false).expect("a default home is created");
assert_eq!(paths.home, root.path().join(".shep"));
assert!(
created,
"the first call must report that it created the home"
);
assert!(
paths.home.is_dir(),
"the home must exist on disk afterwards"
);
let (_, created_again) =
ensure_home_at(paths_at(root.path()), false).expect("second call succeeds");
assert!(
!created_again,
"a home that was already there is not newly created"
);
}
#[cfg(unix)]
#[test]
fn creating_startups_default_home_scaffolds_it_like_the_shared_gate() {
let root = tempfile::tempdir().unwrap();
let paths = paths_at(root.path());
let mut out = Vec::new();
let mut err = Vec::new();
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: style::Presentation::BARE,
fmt: cli::Format::Table,
};
create_default_home(&mut streams, paths.clone()).expect("a default home is created");
assert!(paths.home.is_dir());
let written = std::fs::read_to_string(&paths.daemon_config).unwrap();
assert!(written.contains("[interpreters]"), "{written}");
create_default_home(&mut streams, paths).expect("a home already there is left alone");
}
#[cfg(unix)]
#[test]
fn an_explicitly_named_missing_home_is_refused_and_left_alone() {
let root = tempfile::tempdir().unwrap();
let paths = paths_at(&root.path().join("srv").join("typo"));
let named = paths.home.clone();
let refusal = ensure_home_at(paths, true).expect_err("a named missing home is refused");
assert_eq!(refusal.code(), ExitCode::Usage);
let message = refusal.to_string();
assert!(
message.contains(&named.display().to_string()),
"the refusal must name the path it refused: {message}"
);
assert!(
message.contains("~/.shep"),
"the refusal must point at the default as the way out: {message}"
);
assert!(
!named.exists(),
"a refused path must be left on disk exactly as it was found"
);
}
#[cfg(unix)]
#[test]
fn a_created_home_is_owner_only_from_the_moment_it_exists() {
use std::os::unix::fs::PermissionsExt;
let root = tempfile::tempdir().unwrap();
let (paths, _) = ensure_home_at(paths_at(root.path()), false).unwrap();
let mode = std::fs::metadata(&paths.home).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o700, "a fresh $SHEP_HOME must be owner-only");
}
#[cfg(not(windows))]
const EXPLICIT_HOME: &str = "/tmp/explicit";
#[cfg(windows)]
const EXPLICIT_HOME: &str = r"C:\tmp\explicit";
#[test]
fn explicit_home_field_resolves_to_the_expected_shep_paths() {
let paths = resolve_paths(&global_with_home(Some(EXPLICIT_HOME))).unwrap();
assert_eq!(paths.home, std::path::Path::new(EXPLICIT_HOME));
#[cfg(unix)]
assert_eq!(
paths.socket,
std::path::Path::new("/tmp/explicit/run/shep.sock")
);
#[cfg(windows)]
assert_eq!(paths.socket, std::path::Path::new(&paths.pipe_name()));
}
fn global_with_home(home: Option<&str>) -> cli::GlobalArgs {
cli::GlobalArgs {
host: None,
home: home.map(Into::into),
format: cli::Format::Table,
quiet: false,
style: None,
}
}
#[cfg(unix)]
fn non_utf8_home() -> std::path::PathBuf {
use std::os::unix::ffi::OsStringExt as _;
std::path::PathBuf::from(std::ffi::OsString::from_vec(
b"/tmp/shep-\xff-home".to_vec(),
))
}
#[cfg(unix)]
#[test]
fn a_home_that_is_not_utf8_is_refused_before_any_path_is_derived() {
let home = non_utf8_home();
let global = cli::GlobalArgs {
host: None,
home: Some(home.clone()),
format: cli::Format::Table,
quiet: false,
style: None,
};
let Err(refusal) = resolve_paths(&global) else {
panic!("a home that is not UTF-8 must not resolve a layout");
};
assert!(
matches!(&refusal, HomeRefusal::NotUtf8 { knob, given }
if *knob == HOME_KNOB && given == &home),
"its own refusal, not the relative or unresolved one: {refusal:?}"
);
assert_eq!(refusal.code(), ExitCode::Usage);
let rendered = refusal.to_string();
assert!(
rendered.contains(HOME_KNOB),
"names the spelling to fix: {rendered}"
);
assert!(
rendered.contains("UTF-8"),
"says what is wrong with it: {rendered}"
);
assert!(
!rendered.contains('\u{2014}') && !rendered.contains('\u{2013}'),
"no em or en dash in copy a user reads: {rendered}"
);
}
#[cfg(unix)]
#[test]
fn a_relative_home_is_refused_as_relative_even_when_it_is_also_not_utf8() {
use std::os::unix::ffi::OsStringExt as _;
let home = std::path::PathBuf::from(std::ffi::OsString::from_vec(b"rel-\xff-home".to_vec()));
let global = cli::GlobalArgs {
host: None,
home: Some(home),
format: cli::Format::Table,
quiet: false,
style: None,
};
let Err(refusal) = resolve_paths(&global) else {
panic!("a relative home must not resolve a layout");
};
assert!(
matches!(refusal, HomeRefusal::Relative { .. }),
"the rootless reason wins, since it is the one an operator hits first"
);
}
#[test]
fn a_relative_home_is_refused_before_any_path_is_derived() {
let Err(refusal) = resolve_paths(&global_with_home(Some("rel-home"))) else {
panic!("a relative --home must not resolve a layout");
};
assert!(
matches!(&refusal, HomeRefusal::Relative { knob, given, .. }
if *knob == HOME_KNOB && given == std::path::Path::new("rel-home")),
"a rootless home is its own refusal, not the unresolved one"
);
assert_eq!(
refusal.code(),
ExitCode::Usage,
"a path shep cannot act on is the operator's to fix"
);
let rendered = refusal.to_string();
assert!(
rendered.contains("rel-home"),
"the refusal must quote the path as typed: {rendered}"
);
assert!(
rendered.contains(HOME_KNOB),
"the refusal must name the spelling to fix: {rendered}"
);
let cwd = std::env::current_dir().expect("a current directory");
assert!(
rendered.contains(&cwd.join("rel-home").display().to_string()),
"the remedy must name the absolute form of what was typed: {rendered}"
);
}
#[cfg(windows)]
#[test]
fn a_drive_relative_home_is_refused_on_windows() {
assert!(
matches!(
resolve_paths(&global_with_home(Some(r"\shep"))),
Err(HomeRefusal::Relative { .. })
),
r"`\shep` resolves against whichever drive is current"
);
}
#[cfg(unix)]
#[test]
fn a_home_directory_that_is_not_utf8_is_refused_and_names_its_own_variable() {
use std::os::unix::ffi::OsStringExt as _;
let mangled = |key: &str| {
matches!(key, "HOME" | "USERPROFILE" | "HOMEDRIVE" | "HOMEPATH")
.then(|| OsString::from_vec(b"/home/\xff".to_vec()))
};
let Err(refusal) = resolve_paths_in(&global_with_home(None), &mangled) else {
panic!("a home directory that is not UTF-8 must not resolve a layout");
};
assert!(
matches!(&refusal, HomeRefusal::NotUtf8 { knob, .. } if *knob == HOME_DIR_VAR),
"names the variable that supplied it, not the --home knob: {refusal:?}"
);
assert_eq!(refusal.code(), ExitCode::Usage);
assert!(
refusal.to_string().contains("UTF-8"),
"says what is wrong with it"
);
}
#[test]
fn a_relative_home_directory_is_refused_and_names_its_own_variable() {
let rootless = |key: &str| {
matches!(key, "HOME" | "USERPROFILE" | "HOMEDRIVE" | "HOMEPATH")
.then(|| OsString::from("ada"))
};
let Err(refusal) = resolve_paths_in(&global_with_home(None), &rootless) else {
panic!("a rootless home directory must not resolve a layout");
};
assert!(
matches!(&refusal, HomeRefusal::Relative { knob, given, .. }
if *knob == HOME_DIR_VAR && given == std::path::Path::new("ada")),
"the refusal must carry the home directory as supplied, not the joined `.shep`"
);
let rendered = refusal.to_string();
assert!(
rendered.contains(HOME_DIR_VAR),
"an operator cannot fix `--home` when `--home` is not what said it: {rendered}"
);
assert!(
!rendered.contains(HOME_KNOB),
"naming a knob the operator did not touch sends them to the wrong fix: {rendered}"
);
let cwd = std::env::current_dir().expect("a current directory");
assert!(
rendered.contains(&cwd.join("ada").display().to_string()),
"the remedy must name the absolute form of what was supplied: {rendered}"
);
}
#[test]
fn an_explicit_home_resolves_with_no_home_directory_at_all() {
let nothing = |_: &str| None;
let paths = resolve_paths_in(&global_with_home(Some(EXPLICIT_HOME)), ¬hing)
.expect("--home names a root on its own");
assert_eq!(
paths.home,
std::path::Path::new(EXPLICIT_HOME),
"the empty placeholder must not reach the resolved home"
);
assert!(
!paths.snapshot.starts_with(".shep"),
"a path rooted at the empty placeholder would start with `.shep`: {}",
paths.snapshot.display()
);
}
#[test]
fn an_absolute_home_directory_still_resolves_the_default_home() {
let rooted = |key: &str| (key == "HOME").then(|| OsString::from(EXPLICIT_HOME));
let paths = resolve_paths_in(&global_with_home(None), &rooted)
.expect("an absolute home directory resolves the default home");
assert_eq!(
paths.home,
std::path::Path::new(EXPLICIT_HOME).join(".shep")
);
}
#[test]
fn a_status_line_is_skipped_for_any_argv_that_names_a_home_or_a_host() {
let argv = |tokens: &[&str]| tokens.iter().map(OsString::from).collect::<Vec<_>>();
for named in [
&["shep", "--host", "amp2"][..],
&["shep", "--host=amp2"],
&["shep", "--host", "amp2", "--help"],
&["shep", "--format", "json", "--host", "amp2"],
&["shep", "--home", "/tmp/h"],
&["shep", "--home=/tmp/h"],
] {
assert!(names_another_shepherd(&argv(named)), "{named:?}");
}
for unnamed in [
&["shep"][..],
&["shep", "--help"],
&["shep", "--format", "json"],
&["shep", "--hostile"],
&["shep", "--homes=1"],
] {
assert!(!names_another_shepherd(&argv(unnamed)), "{unnamed:?}");
}
}