use super::*;
use std::fs;
fn test_root(name: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"dracon-quarantine-test-{}-{}",
std::process::id(),
name
))
}
fn fixture_dir(root: &Path) -> PathBuf {
let src = root.join("work").join("proj");
fs::create_dir_all(src.join("nested")).unwrap();
fs::write(src.join("a.txt"), b"hello").unwrap();
fs::write(src.join("nested").join("b.txt"), b"world!").unwrap();
src
}
fn cleanup(root: &Path) {
let _ = fs::remove_dir_all(root);
}
#[test]
fn quarantine_move_list_restore_roundtrip() {
let root = test_root("roundtrip");
let src = fixture_dir(&root);
let qdir = root.join("q");
let manifest = crate::quarantine_move(&src, &qdir, &[]).unwrap();
assert_eq!(manifest.files, 2);
assert_eq!(manifest.bytes, 11);
assert!(!src.exists());
let list = crate::quarantine_list(&qdir, 30).unwrap();
assert_eq!(list.entries.len(), 1);
assert_eq!(list.entries[0].origin, manifest.origin);
assert!(!list.entries[0].expired);
let restored = crate::quarantine_restore(&qdir, &manifest.name).unwrap();
assert_eq!(restored, PathBuf::from(&manifest.origin));
assert_eq!(fs::read(src.join("a.txt")).unwrap(), b"hello");
assert!(!src.join(".quarantine.json").exists());
let list = crate::quarantine_list(&qdir, 30).unwrap();
assert!(list.entries.is_empty());
cleanup(&root);
}
#[cfg(unix)]
#[test]
fn quarantine_restore_copy_path_drops_manifest_and_verifies() {
if !std::path::Path::new("/dev/shm").is_dir() {
return;
}
let root = test_root("xdev");
let src = fixture_dir(&root);
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let qdir = std::path::PathBuf::from(format!(
"/dev/shm/dracon-quarantine-test-{}-{stamp}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&qdir);
let manifest = crate::quarantine_move(&src, &qdir, &[]).unwrap();
assert!(!src.exists(), "move must vacate the source");
let restored = crate::quarantine_restore(&qdir, &manifest.name).unwrap();
assert_eq!(restored, PathBuf::from(&manifest.origin));
assert_eq!(std::fs::read(src.join("a.txt")).unwrap(), b"hello");
assert!(
!src.join(".quarantine.json").exists(),
"restored tree must not contain a stray manifest"
);
let _ = std::fs::remove_dir_all(&qdir);
cleanup(&root);
}
#[cfg(unix)]
#[test]
fn quarantine_move_refuses_symlink() {
use std::os::unix::fs::symlink;
let root = test_root("symlink");
let src = fixture_dir(&root);
let link = root.join("linkdir");
symlink(&src, &link).unwrap();
let err = crate::quarantine_move(&link, &root.join("q"), &[]).unwrap_err();
assert!(format!("{err:#}").contains("symlink"));
cleanup(&root);
}
#[test]
fn quarantine_restore_refuses_existing_origin() {
let root = test_root("exists");
let src = fixture_dir(&root);
let qdir = root.join("q");
let manifest = crate::quarantine_move(&src, &qdir, &[]).unwrap();
fs::create_dir_all(&src).unwrap();
let err = crate::quarantine_restore(&qdir, &manifest.name).unwrap_err();
assert!(format!("{err:#}").contains("already exists"));
assert!(qdir.join(&manifest.name).join(".quarantine.json").exists());
cleanup(&root);
}
#[test]
fn quarantine_restore_refuses_traversal_names() {
let root = test_root("traversal");
let qdir = root.join("q");
fs::create_dir_all(&qdir).unwrap();
for hostile in ["../evil", "..", ".", "", "a/b", "/abs", "a\\b", "a\0b"] {
let err = crate::quarantine_restore(&qdir, hostile).unwrap_err();
assert!(
format!("{err:#}").contains("invalid quarantine entry name"),
"name {hostile:?} must be refused"
);
}
cleanup(&root);
}
#[cfg(unix)]
#[test]
fn quarantine_restore_refuses_smuggled_symlink() {
use std::os::unix::fs::symlink;
let root = test_root("smlink");
let outside = root.join("outside");
fs::create_dir_all(&outside).unwrap();
let qdir = root.join("q");
fs::create_dir_all(&qdir).unwrap();
symlink(&outside, qdir.join("linked")).unwrap();
let err = crate::quarantine_restore(&qdir, "linked").unwrap_err();
let msg = format!("{err:#}");
assert!(
msg.contains("symlink") || msg.contains("no manifest"),
"smuggled symlink must be refused: {msg}"
);
assert!(outside.is_dir() && !outside.join(".quarantine.json").exists());
cleanup(&root);
}
#[test]
fn quarantine_expire_deletes_only_past_ttl() {
let root = test_root("expire");
let src = fixture_dir(&root);
let qdir = root.join("q");
let manifest = crate::quarantine_move(&src, &qdir, &[]).unwrap();
let aged = crate::QuarantineManifest {
name: manifest.name.clone(),
origin: manifest.origin.clone(),
moved_at_unix: crate::now_unix().saturating_sub(40 * 86_400),
bytes: manifest.bytes,
files: manifest.files,
};
let entry_dir = qdir.join(&manifest.name);
fs::write(
entry_dir.join(".quarantine.json"),
serde_json::to_string_pretty(&aged).unwrap(),
)
.unwrap();
let listed = crate::quarantine_list(&qdir, 60).unwrap();
assert!(!listed.entries[0].expired);
let listed = crate::quarantine_list(&qdir, 30).unwrap();
assert!(listed.entries[0].expired);
let dry = crate::quarantine_expire(&qdir, 30, false).unwrap();
assert_eq!(dry, vec![manifest.name.clone()]);
assert!(entry_dir.exists());
let gone = crate::quarantine_expire(&qdir, 30, true).unwrap();
assert_eq!(gone, vec![manifest.name.clone()]);
assert!(!entry_dir.exists());
cleanup(&root);
}
#[test]
fn quarantine_expire_zero_ttl_disables() {
let root = test_root("zerottl");
let src = fixture_dir(&root);
let qdir = root.join("q");
let manifest = crate::quarantine_move(&src, &qdir, &[]).unwrap();
let out = crate::quarantine_expire(&qdir, 0, true).unwrap();
assert!(out.is_empty());
assert!(qdir.join(&manifest.name).exists());
cleanup(&root);
}
#[cfg(unix)]
#[test]
fn quarantine_expire_skips_escaped_entries_without_aborting_batch() {
let root = test_root("expireskip");
let src = fixture_dir(&root);
let qdir = root.join("q");
let manifest = crate::quarantine_move(&src, &qdir, &[]).unwrap();
let aged = crate::QuarantineManifest {
name: manifest.name.clone(),
origin: manifest.origin.clone(),
moved_at_unix: crate::now_unix().saturating_sub(40 * 86_400),
bytes: manifest.bytes,
files: manifest.files,
};
let entry_dir = qdir.join(&manifest.name);
fs::write(
entry_dir.join(".quarantine.json"),
serde_json::to_string_pretty(&aged).unwrap(),
)
.unwrap();
let outside = root.join("outside");
fs::create_dir_all(&outside).unwrap();
fs::write(
outside.join(".quarantine.json"),
serde_json::to_string_pretty(&aged).unwrap(),
)
.unwrap();
std::os::unix::fs::symlink(&outside, qdir.join("evil")).unwrap();
let removed = crate::quarantine_expire(&qdir, 30, true).unwrap();
assert_eq!(removed, vec![manifest.name.clone()]);
assert!(!entry_dir.exists(), "expired entry removed");
assert!(
fs::symlink_metadata(qdir.join("evil")).is_ok(),
"escaped entry must survive"
);
assert!(outside.is_dir(), "escape target untouched");
cleanup(&root);
}
#[test]
fn quarantine_first_remove_same_fs_frees_nothing() {
let root = test_root("firstsame");
let src = fixture_dir(&root);
let qdir = root.join("q");
let (manifest, freed) = crate::quarantine_first_remove(&src, &qdir, &[]).unwrap();
assert_eq!(manifest.bytes, 11);
assert_eq!(freed, 0);
assert!(!src.exists());
cleanup(&root);
}
#[test]
fn quarantine_first_remove_cross_fs_frees_bytes() {
let shm = PathBuf::from("/dev/shm");
let probe = shm.join(format!("dracon-q-probe-{}", std::process::id()));
if fs::write(&probe, b"x").is_err() {
return;
}
let _ = fs::remove_file(&probe);
let root = test_root("firstcross");
let src = fixture_dir(&root);
let qdir = shm.join(format!("dracon-q-test-{}", std::process::id()));
let (_manifest, freed) = crate::quarantine_first_remove(&src, &qdir, &[]).unwrap();
assert_eq!(freed, 11);
let _ = fs::remove_dir_all(&qdir);
cleanup(&root);
}
#[test]
fn quarantine_list_empty_root() {
let root = test_root("empty");
let list = crate::quarantine_list(&root.join("q"), 30).unwrap();
assert!(list.entries.is_empty());
assert_eq!(list.total_bytes, 0);
cleanup(&root);
}