use super::support::{
Fixture, Observed, TIMED_OUT_SIGNAL, bounded, bounded_for, copy_executable, direct, executable,
fixture, link_real_git, observe, remove,
};
use std::ffi::OsString;
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
use std::process::Command;
fn path_of(fixture: &Fixture, entries: &[&str]) -> OsString {
let mut path: OsString = OsString::new();
for entry in entries {
if !path.is_empty() {
path.push(":");
}
if entry.starts_with('/') {
path.push(entry);
} else {
path.push(fixture.root.join(entry));
}
}
return path;
}
fn assert_banner_follows_git_line(stdout: &[u8]) {
let text: String = String::from_utf8_lossy(stdout).into_owned();
let banner: &str = text
.strip_prefix("git version 2.56.0\n")
.expect("Git's version line comes first");
assert!(
banner.starts_with(concat!("monochromatic-git-policy-cli ", env!("CARGO_PKG_VERSION"), ", shadowing git at /")),
"{text}"
);
assert_eq!(banner.matches('\n').count(), 1, "{text}");
assert!(banner.ends_with('\n'), "{text}");
}
fn plant_wrapper_forms(fixture: &Fixture) {
for directory in ["hard", "copy-early", "copy-late", "chain", "nested"] {
std::fs::create_dir(fixture.root.join(directory)).expect("directory");
}
std::fs::hard_link(&fixture.wrapper, fixture.root.join("hard/git")).expect("hard link");
copy_executable(
fixture.wrapper.as_path(),
fixture.root.join("copy-early/git").as_path(),
);
copy_executable(
fixture.wrapper.as_path(),
fixture.root.join("copy-late/git").as_path(),
);
std::os::unix::fs::symlink(fixture.root.join("bin/git"), fixture.root.join("chain/git"))
.expect("chained link");
std::os::unix::fs::symlink(
fixture.root.join("bin"),
fixture.root.join("nested/bin-link"),
)
.expect("directory link");
}
#[test]
fn time_bound_detects_a_forwarding_loop() {
let fixture: Fixture = fixture("loop-control");
std::fs::create_dir(fixture.root.join("loop")).expect("directory");
let looping: PathBuf = fixture.root.join("loop/git");
executable(looping.as_path(), b"#!/bin/sh\nexec \"$0\" \"$@\"\n");
let status = bounded_for(
&fixture,
looping.as_path(),
path_of(&fixture, &["loop"]).as_os_str(),
"1",
)
.arg("--version")
.status()
.expect("bounded loop");
assert_eq!(
status.signal(),
Some(TIMED_OUT_SIGNAL),
"a forwarding loop must be killed at the bound"
);
remove(&fixture);
}
#[test]
fn wrapper_never_selects_itself_or_a_copy_of_itself() {
let fixture: Fixture = fixture("self-exclusion");
plant_wrapper_forms(&fixture);
link_real_git(&fixture);
let expected: Observed = observe(
direct(&fixture).args(["rev-parse", "--sq-quote", "reached real git"]),
b"",
);
assert_eq!(expected.code, Some(0));
assert!(expected.stdout.ends_with(b"reached real git'\n"));
let path: OsString = path_of(
&fixture,
&[
"bin",
"bin",
"chain",
"hard",
"copy-early",
"nested/bin-link",
"missing",
"real",
"copy-late",
"bin",
],
);
for started in [
"bin/git",
"chain/git",
"hard/git",
"copy-early/git",
"copy-late/git",
"nested/bin-link/git",
"wrapper/cli-git-native",
] {
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join(started).as_path(),
path.as_os_str(),
)
.args(["rev-parse", "--sq-quote", "reached real git"]),
b"",
);
assert_eq!(observed, expected, "{started}");
}
let relative: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
OsString::from("bin:copy-early::real").as_os_str(),
)
.arg("--version"),
b"",
);
assert_banner_follows_git_line(&relative.stdout);
assert_eq!(relative.code, Some(0));
remove(&fixture);
}
#[test]
fn only_wrappers_on_path_is_a_reported_failure() {
let fixture: Fixture = fixture("no-real-git");
plant_wrapper_forms(&fixture);
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "hard", "copy-early", "chain", "missing"]).as_os_str(),
)
.arg("--version"),
b"",
);
let text: String = String::from_utf8_lossy(observed.stderr.as_slice()).into_owned();
assert_eq!(observed.code, Some(2), "{text}");
assert!(observed.stdout.is_empty());
assert!(
text.starts_with(
"cli-git: Could not find a real Git executable after examining 5 PATH candidates and skipping 4 cli-git wrappers, \
so no Git command was run. PATH is "
),
"{text}"
);
for form in ["bin", "hard", "copy-early", "chain"] {
assert!(text.contains(fixture.root.join(form).join("git").to_string_lossy().as_ref()), "{form}: {text}");
}
assert!(text.contains("To fix it, install Git"), "{text}");
remove(&fixture);
}
#[test]
fn typescript_wrapper_launcher_is_skipped() {
let fixture: Fixture = fixture("typescript-shim");
link_real_git(&fixture);
std::fs::create_dir(fixture.root.join("shim")).expect("directory");
executable(
fixture.root.join("shim/git").as_path(),
b"#!/bin/sh\n# node_modules/@monochromatic-dev/git-policy-cli/dist/final/node/index.mjs\ntouch \"$(dirname \"$0\")/executed\"\nexit 99\n",
);
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "shim", "real"]).as_os_str(),
)
.arg("--version"),
b"",
);
assert_banner_follows_git_line(&observed.stdout);
assert_eq!(observed.code, Some(0));
assert!(!fixture.root.join("shim/executed").exists());
remove(&fixture);
}
#[test]
fn different_wrapper_build_stops_instead_of_looping() {
let fixture: Fixture = fixture("other-build");
std::fs::create_dir(fixture.root.join("other")).expect("directory");
let other: PathBuf = fixture.root.join("other/git");
let mut bytes: Vec<u8> = std::fs::read(&fixture.wrapper).expect("wrapper bytes");
bytes.push(0);
executable(other.as_path(), bytes.as_slice());
let ran = Command::new(&other)
.arg("--version")
.env_clear()
.env("PATH", "/usr/bin")
.output();
assert_banner_follows_git_line(&ran.expect("modified build still runs").stdout);
let observed: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "other"]).as_os_str(),
)
.arg("--version"),
b"",
);
let text: String = String::from_utf8_lossy(observed.stderr.as_slice()).into_owned();
assert_eq!(observed.code, Some(2), "{text}");
assert!(observed.stdout.is_empty());
assert!(text.contains(other.to_string_lossy().as_ref()), "{text}");
assert!(text.contains("was started as Git by a cli-git wrapper"), "{text}");
assert!(text.contains("never runs itself as Git"), "{text}");
link_real_git(&fixture);
let with_git: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "other", "real"]).as_os_str(),
)
.arg("--version"),
b"",
);
assert_eq!(with_git.code, Some(2));
assert_eq!(with_git.stdout, Vec::<u8>::new());
let conventional: Observed = observe(
bounded(
&fixture,
fixture.root.join("bin/git").as_path(),
path_of(&fixture, &["bin", "other", "/usr/bin"]).as_os_str(),
)
.arg("--version"),
b"",
);
assert_banner_follows_git_line(&conventional.stdout);
assert_eq!(conventional.code, Some(0));
remove(&fixture);
}