#![allow(
clippy::too_many_lines,
clippy::similar_names,
clippy::items_after_statements,
clippy::doc_markdown,
clippy::doc_lazy_continuation
)]
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn snapdir_bin() -> PathBuf {
assert_cmd::cargo::cargo_bin("snapdir")
}
fn temp_dir(tag: &str) -> PathBuf {
let mut dir = std::env::temp_dir();
dir.push(format!(
"snapdir-dxhelp-{tag}-{}-{:?}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&dir).expect("create temp dir");
dir
}
fn run_raw(args: &[&str], cache: &Path) -> Output {
Command::new(snapdir_bin())
.args(args)
.env("SNAPDIR_CACHE_DIR", cache)
.env_remove("SNAPDIR_STORE")
.env_remove("SNAPDIR_OBJECTS_STORE")
.output()
.expect("run snapdir")
}
fn stdout_of(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn stderr_of(out: &Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
fn build_tree(dir: &Path, leaves: &[(&str, &[u8])]) {
for (rel, bytes) in leaves {
let path = dir.join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&path, bytes).unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).unwrap();
}
fs::set_permissions(dir, fs::Permissions::from_mode(0o755)).unwrap();
}
#[test]
fn verify_help_does_not_claim_staged_and_mentions_store() {
let cache = temp_dir("verify-help");
let out = run_raw(&["verify", "--help"], &cache);
assert!(
out.status.success(),
"`verify --help` must exit 0; stderr: {}",
stderr_of(&out)
);
let help = stdout_of(&out);
let help_lc = help.to_lowercase();
assert!(
!help_lc.contains("staged"),
"`verify --help` must NOT describe the snapshot as \"staged\" \
(verify checks the STORE, requiring --store/--id; \"staged\" is false \
reassurance). Full help:\n{help}"
);
assert!(
help_lc.contains("store"),
"`verify --help` must indicate it verifies a snapshot in the STORE \
(mention \"store\"). Full help:\n{help}"
);
}
#[test]
fn invalid_store_protocol_error_lists_file_scheme() {
let cache = temp_dir("badproto-push-cache");
let src = temp_dir("badproto-push-src");
build_tree(&src, &[("a.txt", b"hello")]);
let src_str = src.to_string_lossy().into_owned();
let out = run_raw(&["push", "--store", "/no/scheme/here", &src_str], &cache);
assert!(
!out.status.success(),
"push with an unrecognized --store protocol must fail; stderr: {}",
stderr_of(&out)
);
let err = stderr_of(&out);
let err_lc = err.to_lowercase();
assert!(
err_lc.contains("invalid store protocol") || err_lc.contains("protocol"),
"expected the invalid-store-protocol error; got: {err}"
);
assert!(
err_lc.contains("file://"),
"the invalid-store-protocol error must LIST the valid scheme(s) and \
include `file://` so the user can discover what to type; got: {err}"
);
}
#[test]
fn invalid_store_protocol_error_on_sync_to_lists_file_scheme() {
let cache = temp_dir("badproto-sync-cache");
let id = "0".repeat(64);
let from = temp_dir("badproto-sync-from");
let from_url = format!("file://{}", from.display());
let out = run_raw(
&[
"sync",
"--id",
&id,
"--from",
&from_url,
"--to",
"/no/scheme/dest",
],
&cache,
);
assert!(
!out.status.success(),
"sync with an unrecognized --to protocol must fail; stderr: {}",
stderr_of(&out)
);
let err = stderr_of(&out);
let err_lc = err.to_lowercase();
assert!(
err_lc.contains("invalid store protocol") || err_lc.contains("protocol"),
"expected the invalid-store-protocol error on the --to side; got: {err}"
);
assert!(
err_lc.contains("file://"),
"the invalid-store-protocol error (sync --to) must also list `file://`; \
got: {err}"
);
}
#[test]
fn verify_help_names_both_required_flags_and_no_staged() {
let cache = temp_dir("verify-help-flags");
let out = run_raw(&["verify", "--help"], &cache);
assert!(
out.status.success(),
"`verify --help` must exit 0; stderr: {}",
stderr_of(&out)
);
let help = stdout_of(&out);
let help_lc = help.to_lowercase();
assert!(
!help_lc.contains("staged"),
"`verify --help` must NOT describe the snapshot as \"staged\"; full help:\n{help}"
);
assert!(
help.contains("--store"),
"`verify --help` must name the required `--store` flag; full help:\n{help}"
);
assert!(
help.contains("--id"),
"`verify --help` must name the required `--id` flag; full help:\n{help}"
);
}
#[test]
fn invalid_store_protocol_error_on_sync_from_lists_file_scheme() {
let cache = temp_dir("badproto-from-cache");
let id = "0".repeat(64);
let to = temp_dir("badproto-from-to");
let to_url = format!("file://{}", to.display());
let out = run_raw(
&[
"sync",
"--id",
&id,
"--from",
"/no/scheme/src",
"--to",
&to_url,
],
&cache,
);
assert!(
!out.status.success(),
"sync with an unrecognized --from protocol must fail; stderr: {}",
stderr_of(&out)
);
let err = stderr_of(&out);
let err_lc = err.to_lowercase();
assert!(
err_lc.contains("invalid store protocol") || err_lc.contains("protocol"),
"expected the invalid-store-protocol error on the --from side; got: {err}"
);
assert!(
err_lc.contains("file://"),
"the invalid-store-protocol error (sync --from) must also list `file://`; got: {err}"
);
}
#[test]
fn invalid_objects_store_protocol_error_lists_file_scheme() {
let cache = temp_dir("badproto-obj-cache");
let src = temp_dir("badproto-obj-src");
build_tree(&src, &[("a.txt", b"hello")]);
let src_str = src.to_string_lossy().into_owned();
let store = temp_dir("badproto-obj-store");
let store_url = format!("file://{}", store.display());
let out = run_raw(
&[
"push",
"--store",
&store_url,
"--objects-store",
"/no/scheme/obj",
&src_str,
],
&cache,
);
assert!(
!out.status.success(),
"push with an unrecognized --objects-store protocol must fail; stderr: {}",
stderr_of(&out)
);
let err = stderr_of(&out);
let err_lc = err.to_lowercase();
assert!(
err_lc.contains("invalid store protocol") || err_lc.contains("protocol"),
"expected the invalid-store-protocol error on the --objects-store side; got: {err}"
);
assert!(
err_lc.contains("file://"),
"the invalid-store-protocol error (--objects-store) must also list `file://`; got: {err}"
);
}
#[test]
fn bare_path_vs_unknown_scheme_route_to_distinct_errors() {
let bare_cache = temp_dir("split-bare-cache");
let bare_src = temp_dir("split-bare-src");
build_tree(&bare_src, &[("a.txt", b"hi")]);
let bare_src_str = bare_src.to_string_lossy().into_owned();
let bare = run_raw(
&["push", "--store", "/no/scheme/here", &bare_src_str],
&bare_cache,
);
assert!(
!bare.status.success(),
"bare-path --store must fail; stderr: {}",
stderr_of(&bare)
);
let bare_err = stderr_of(&bare);
let bare_err_lc = bare_err.to_lowercase();
assert!(
bare_err_lc.contains("invalid store protocol"),
"a bare scheme-less path must hit the invalid-store-protocol branch; got: {bare_err}"
);
assert!(
bare_err_lc.contains("file://"),
"the bare-path invalid-protocol error must name `file://`; got: {bare_err}"
);
let ext_cache = temp_dir("split-ext-cache");
let ext_src = temp_dir("split-ext-src");
build_tree(&ext_src, &[("a.txt", b"hi")]);
let ext_src_str = ext_src.to_string_lossy().into_owned();
let ext = run_raw(
&["push", "--store", "notaproto://x", &ext_src_str],
&ext_cache,
);
assert!(
!ext.status.success(),
"unknown scheme push must still fail (no such helper); stderr: {}",
stderr_of(&ext)
);
let ext_err = stderr_of(&ext);
let ext_err_lc = ext_err.to_lowercase();
assert!(
!ext_err_lc.contains("invalid store protocol"),
"a well-formed unknown `scheme://` must NOT hit the invalid-store-protocol \
branch (it is a valid scheme that routes to an external helper); got: {ext_err}"
);
assert!(
!ext_err_lc.contains("file://"),
"the unknown-scheme route must NOT carry the `file://` invalid-protocol hint \
(it would wrongly suggest a local store for a well-formed remote URI); got: {ext_err}"
);
assert!(
ext_err_lc.contains("notaproto"),
"the unknown-scheme error should reference the requested adapter \
(`snapdir-notaproto-store`); got: {ext_err}"
);
}