test: cover the store and the exit codes before moving any of it

This commit is contained in:
2026-09-09 12:27:51 +00:00
parent 2563d56595
commit d9439253fd
3 changed files with 568 additions and 0 deletions

262
tests/link_store.rs Normal file
View File

@@ -0,0 +1,262 @@
// what the store is for: a path a sandbox should not read living somewhere it
// cannot. every case here is one that was once wrong.
mod support;
use std::fs;
use support::Case;
#[test]
fn a_path_moves_into_the_store_and_reads_back_through_the_link() {
let case = Case::new("roundtrip");
case.write(".env", b"SECRET=1\n");
case.write("secrets/token", b"tok\n");
case.gitignore(".env\nsecrets/\n");
case.ahab(&["link", "add", ".env", "secrets"]).ok();
assert!(case.is_symlink(".env"));
assert_eq!(fs::read(case.path(".env")).unwrap(), b"SECRET=1\n");
assert_eq!(fs::read(case.path("secrets/token")).unwrap(), b"tok\n");
case.ahab(&["link", "check", "--exit-code"]).ok();
case.ahab(&["link", "restore", "--all"]).ok();
assert!(!case.is_symlink(".env"));
assert_eq!(fs::read(case.path(".env")).unwrap(), b"SECRET=1\n");
}
#[test]
fn add_refuses_a_symlink_that_already_leads_out_of_the_repository() {
let case = Case::new("launder");
fs::write(case.outside.join("key"), b"KEY\n").unwrap();
case.link(&case.path("cache"), &case.outside);
// moving the link would move the pointer and leave the contents there,
// and check would then see a link into the store and call it clean
case.ahab(&["link", "add", "cache"])
.failed()
.says("outside the repository");
case.ahab(&["link", "check", "--exit-code"]).code_is(4);
}
#[test]
fn check_reports_a_store_entry_that_leads_back_out() {
let case = Case::new("poisoned");
fs::create_dir_all(&case.store).unwrap();
fs::write(case.outside.join("key"), b"KEY\n").unwrap();
// the state an older ahab left: the store holds a way back out
case.link(&case.store.join("cache"), &case.outside);
case.link(&case.path("cache"), &case.store.join("cache"));
case.ahab(&["link", "check", "--exit-code"]).code_is(4);
}
#[test]
fn check_reports_a_tracked_symlink_leading_out() {
let case = Case::new("tracked-link");
fs::create_dir_all(case.outside.join("aws")).unwrap();
case.link(&case.path("awsdir"), &case.outside.join("aws"));
case.git(&["add", "-f", "awsdir"]);
case.git(&["commit", "-qm", "commit a symlink out"]);
// git tracks symlinks, so this is in no untracked or ignored listing
case.ahab(&["link", "check", "--exit-code"]).code_is(4);
case.ahab(&["link", "check", "--porcelain"]).says("T>");
// and add cannot fix it, so saying so is all check can do
case.ahab(&["link", "add", "awsdir"])
.failed()
.says("tracked by git");
}
#[test]
fn a_stored_symlink_is_one_entry_rather_than_a_tree_to_walk() {
let case = Case::new("walk-out");
fs::create_dir_all(case.outside.join("private")).unwrap();
fs::write(case.outside.join("private/diary"), b"x\n").unwrap();
fs::write(case.outside.join(".netrc"), b"x\n").unwrap();
fs::create_dir_all(&case.store).unwrap();
case.link(&case.store.join("cache"), &case.outside);
let run = case.ahab(&["link", "list"]);
run.ok().says("cache");
// is_dir() would follow the link and enumerate what is behind it
run.silent_about(".netrc");
run.silent_about("diary");
}
#[test]
fn a_store_inside_the_repository_is_refused_however_it_is_spelled() {
let case = Case::new("store-inside");
case.write(".env", b"SECRET=1\n");
case.gitignore(".env\n");
// named directly
case.ahab(&["link", "add", "--store", "./within", ".env"])
.failed()
.says("inside the repository");
// and reached through a symlink, which a prefix test does not catch
case.mkdir("within");
let sneaky = case.repo.parent().unwrap().join("sneaky");
case.link(&sneaky, &case.path("within"));
let run = case.ahab(&[
"link".as_ref(),
"add".as_ref(),
"--store".as_ref(),
sneaky.as_os_str(),
".env".as_ref(),
]);
run.failed().says("inside the repository");
assert!(!case.is_symlink(".env"));
}
#[test]
fn a_relative_store_root_still_resolves_from_anywhere() {
let case = Case::new("relative-store");
case.write("sub/.env", b"SECRET=1\n");
case.gitignore("sub/.env\n");
case.ahab(&["link", "add", "--store", "../store", "sub/.env"])
.ok();
// the target is written into the symlink, so a relative one would resolve
// from the link's own directory rather than the working one
assert!(case.is_symlink("sub/.env"));
assert_eq!(fs::read(case.path("sub/.env")).unwrap(), b"SECRET=1\n");
}
#[test]
fn the_store_directories_are_the_owners_alone() {
let case = Case::new("modes");
case.write("deep/nested/.env", b"SECRET=1\n");
case.gitignore("deep\n");
case.ahab(&["link", "add", "deep/nested/.env"]).ok();
for dir in [
case.store.as_path(),
&case.store.join("deep"),
&case.store.join("deep/nested"),
] {
assert_eq!(case.mode(dir), 0o700, "{}", dir.display());
}
}
#[test]
fn a_failed_link_puts_the_payload_back() {
let case = Case::new("rollback");
case.write("x.env", b"SECRET=1\n");
// ahab's own temp name, but a real file: it must not be removed, and the
// payload must not be left in the store with nothing pointing at it
case.write("x.env.ahab-tmp", b"THE PROJECT OWNS THIS\n");
case.gitignore("x.env\n");
case.ahab(&["link", "add", "x.env"]).failed();
assert!(!case.is_symlink("x.env"));
assert_eq!(fs::read(case.path("x.env")).unwrap(), b"SECRET=1\n");
assert_eq!(
fs::read(case.path("x.env.ahab-tmp")).unwrap(),
b"THE PROJECT OWNS THIS\n"
);
assert!(!case.store.join("x.env").exists());
}
#[test]
fn a_directory_of_symlinks_out_keeps_its_destinations() {
let case = Case::new("collapse");
fs::write(case.outside.join("a"), b"A\n").unwrap();
case.link(&case.path("bundle/one"), &case.outside.join("a"));
// collapsing to `bundle/` would drop both the destination and the code
let run = case.ahab(&["link", "check", "--porcelain"]);
run.code_is(0).says("?>").says("bundle/one");
}
#[test]
fn a_filename_that_is_not_utf_8_is_reported_and_moved_as_itself() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let case = Case::new("latin1");
let name = OsStr::from_bytes(b"caf\xe9.env");
fs::write(case.repo.join(name), b"SECRET=1\n").unwrap();
// the whole listing used to fail on the one entry
let run = case.ahab(&["link", "check", "-z"]);
run.code_is(0);
assert!(
run.stdout.windows(4).any(|w| w == b"caf\xe9"),
"-z must write the bytes the name actually has"
);
case.gitignore("caf\u{e9}.env\n");
case.ahab(&["link".as_ref(), "add".as_ref(), name]).ok();
assert!(
fs::symlink_metadata(case.repo.join(name))
.unwrap()
.is_symlink()
);
}
#[test]
fn a_tracked_filename_that_is_not_utf_8_is_still_refused() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let case = Case::new("latin1-tracked");
let name = OsStr::from_bytes(b"caf\xe9.env");
fs::write(case.repo.join(name), b"TRACKED\n").unwrap();
case.git(&["add".as_ref(), "-f".as_ref(), name]);
case.git(&["commit", "-qm", "track a latin-1 name"]);
// asked about lossily, git answers about a path nothing has and says
// "not tracked", and a tracked file leaves the working tree
case.ahab(&["link".as_ref(), "add".as_ref(), name])
.failed()
.says("tracked by git");
assert!(
!fs::symlink_metadata(case.repo.join(name))
.unwrap()
.is_symlink()
);
}
#[test]
fn a_pathspec_is_asked_about_as_a_name_not_a_pattern() {
let case = Case::new("pathspec");
// git reads pathspec magic after `--` too, so this used to have git list
// every tracked file *except* the named one, which read as "it is tracked"
case.write(":!untracked.env", b"SECRET=1\n");
case.gitignore(":!untracked.env\n");
case.ahab(&["link", "add", ":!untracked.env"]).ok();
assert!(case.is_symlink(":!untracked.env"));
}
#[test]
fn distinct_remotes_do_not_share_one_store_directory() {
let case = Case::new("remotes");
case.git(&[
"remote",
"set-url",
"origin",
"https://git.example.org/a/my_api",
]);
let plain = case.ahab(&["link", "list"]).out();
case.git(&[
"remote",
"set-url",
"origin",
"https://git.example.org/a/my~api",
]);
let awkward = case.ahab(&["link", "list"]).out();
assert_ne!(plain, awkward);
// the ordinary remote keeps the path it already had
assert!(plain.contains("/a/my_api"), "{plain}");
}