Files
wanderbooks/crates/ledger-cli/tests/setup.rs
T
swaitsandClaude Opus 5.5 a82791789e
check / check (push) Canceled after 0s
P2 slice 2: the data root (plan 3.4, 5.5)
ledger_store::root: one private folder for every book, found without the
environment (--root, else the root setup recorded in a file only this
account can write, else the platform default under the home from the
password database). Every folder made 0700, files 0600; a root that is a
link, another user's, open to others, unmarked, or under a folder another
user could swap is refused; no error names it. Setup keeps a recorded
root, never makes a second root or moves books while one holds any, and
leaves a folder holding other things untouched. Books are created and
adopted one at a time; adoption reads the source under its writer lock
(changing nothing in it), copies into staging, re-hashes every stored
file against its name, checks every named file is there, opens and
verifies the copy with the source's head, imports settings for books
that keep none, syncs, and only then renames it into place.
wanderbooks setup makes the root and never prints where it is. Three
review rounds; mutation on the diff (docs/testing.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-18 14:42:32 -06:00

125 lines
4.0 KiB
Rust

//! `wanderbooks setup`: the books' private folder, made from the command
//! line, with its location never printed.
#![cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::process::Command;
fn wanderbooks() -> Command {
Command::new(env!("CARGO_BIN_EXE_wanderbooks"))
}
fn mode(path: &std::path::Path) -> u32 {
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[test]
fn setup_makes_the_private_folder_and_never_names_it() {
let home = tempfile::Builder::new()
.prefix("wb-setup-")
.tempdir()
.unwrap();
let root = home.path().join(".local/share/wanderbooks");
let out = wanderbooks()
.args(["setup", "--home"])
.arg(home.path())
.env_remove("XDG_DATA_HOME")
.output()
.unwrap();
assert!(out.status.success(), "{out:?}");
let text = String::from_utf8_lossy(&out.stdout);
assert!(text.contains("The books' private folder: made"), "{text}");
assert!(
String::from_utf8_lossy(&out.stderr).contains("--home isn't this account's home"),
"{out:?}"
);
assert!(
!text.contains(home.path().to_str().unwrap()),
"names the folder: {text}"
);
assert!(!String::from_utf8_lossy(&out.stderr).contains(home.path().to_str().unwrap()));
assert_eq!(mode(&root), 0o700);
for sub in ["books", "run", "legacy"] {
assert_eq!(mode(&root.join(sub)), 0o700, "{sub}");
}
// Again: already there.
let out = wanderbooks()
.args(["setup", "--home"])
.arg(home.path())
.env_remove("XDG_DATA_HOME")
.output()
.unwrap();
assert!(
String::from_utf8_lossy(&out.stdout).contains("already there."),
"{out:?}"
);
// Others could open it: made private again.
std::fs::set_permissions(&root, std::fs::Permissions::from_mode(0o755)).unwrap();
let out = wanderbooks()
.args(["setup", "--home"])
.arg(home.path())
.env_remove("XDG_DATA_HOME")
.output()
.unwrap();
assert!(
String::from_utf8_lossy(&out.stdout).contains("made private again"),
"{out:?}"
);
assert_eq!(mode(&root), 0o700);
}
#[test]
fn a_root_elsewhere_is_noted_and_xdg_is_honoured() {
let home = tempfile::Builder::new()
.prefix("wb-setup-xdg-")
.tempdir()
.unwrap();
let xdg = home.path().join("xdg");
let out = wanderbooks()
.args(["setup", "--home"])
.arg(home.path())
.env("XDG_DATA_HOME", &xdg)
.output()
.unwrap();
assert!(out.status.success(), "{out:?}");
assert!(
String::from_utf8_lossy(&out.stdout).contains("noted for next time"),
"{out:?}"
);
assert_eq!(mode(&xdg.join("wanderbooks")), 0o700);
let noted =
std::fs::read_to_string(home.path().join(".local/share/wanderbooks.location")).unwrap();
assert_eq!(
noted.trim_end(),
xdg.join("wanderbooks").display().to_string()
);
}
#[test]
fn setup_refuses_what_it_does_not_know_and_unsafe_folders() {
let out = wanderbooks().args(["setup", "--bogus"]).output().unwrap();
assert_eq!(out.status.code(), Some(2));
let out = wanderbooks().args(["setup", "--root"]).output().unwrap();
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("--root needs a path"));
let home = tempfile::Builder::new()
.prefix("wb-setup-link-")
.tempdir()
.unwrap();
let real = home.path().join("real");
std::fs::create_dir(&real).unwrap();
std::os::unix::fs::symlink(&real, home.path().join("link")).unwrap();
let out = wanderbooks()
.args(["setup", "--home"])
.arg(home.path())
.arg("--root")
.arg(home.path().join("link"))
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1), "{out:?}");
assert!(String::from_utf8_lossy(&out.stderr).contains("is a symlink"));
// A mode comes first.
let out = wanderbooks().args(["--force", "setup"]).output().unwrap();
assert_eq!(out.status.code(), Some(2));
}