Files
herdr/tests/session_delete.rs
Eric YueandJJ Liebig ab15da285e fix: require exact session names before deletion (#4233)
* fix: require exact session names before deletion

refs #3819

* test: isolate session deletion regression tests

refs #3819

---------

Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
2026-09-16 14:41:41 +02:00

181 lines
5.3 KiB
Rust

use std::fs;
use std::path::PathBuf;
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
/// Owns a unique configuration directory; only child CLIs receive its environment.
struct SessionConfig {
root: PathBuf,
}
impl SessionConfig {
fn new() -> Self {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
// Keep Unix socket paths below the host's length limit.
#[cfg(unix)]
let base = PathBuf::from("/tmp");
#[cfg(not(unix))]
let base = std::env::temp_dir();
let root = base.join(format!(
"hs-delete-{}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros(),
NEXT_ID.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir(&root).unwrap();
Self { root }
}
fn session_dir(&self, name: &str) -> PathBuf {
let app_dir = if cfg!(debug_assertions) {
"herdr-dev"
} else {
"herdr"
};
self.root.join(app_dir).join("sessions").join(name)
}
fn create_session(&self, name: &str) -> PathBuf {
let dir = self.session_dir(name);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("session.json"), b"saved session").unwrap();
dir
}
fn delete(&self, name: &str) -> Output {
Command::new(env!("CARGO_BIN_EXE_herdr"))
.args(["session", "delete", name, "--json"])
.env("XDG_CONFIG_HOME", &self.root)
.env_remove("HERDR_SESSION")
.env_remove("HERDR_SOCKET_PATH")
.env_remove("HERDR_CLIENT_SOCKET_PATH")
.output()
.unwrap()
}
}
impl Drop for SessionConfig {
fn drop(&mut self) {
// Cleanup must not cause a second panic if a test assertion failed.
let _ = fs::remove_dir_all(&self.root);
}
}
fn assert_success(output: &Output) -> serde_json::Value {
assert!(
output.status.success(),
"stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let response: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(response["deleted"], true);
response
}
#[test]
fn delete_session_preserves_differently_cased_name() {
let config = SessionConfig::new();
let dir = config.create_session("Foo");
let case_insensitive = config.session_dir("foo").exists();
let output = config.delete("foo");
assert_eq!(
fs::read(dir.join("session.json")).unwrap(),
b"saved session"
);
if case_insensitive {
assert_eq!(output.status.code(), Some(1));
let error: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
assert_eq!(error["error"]["code"], "session_delete_failed");
assert!(error["error"]["message"]
.as_str()
.unwrap()
.contains("exact"));
} else {
assert_success(&output);
}
}
#[test]
fn delete_session_removes_exact_stopped_name_only() {
let config = SessionConfig::new();
let dir = config.create_session("Foo");
let other = config.create_session("other");
let response = assert_success(&config.delete("Foo"));
assert_eq!(response["session"]["name"], "Foo");
assert_eq!(response["session"]["running"], false);
assert!(!dir.exists());
assert_eq!(
fs::read(other.join("session.json")).unwrap(),
b"saved session"
);
}
#[test]
fn delete_session_missing_name_remains_idempotent() {
let config = SessionConfig::new();
assert_success(&config.delete("missing"));
config.create_session("other");
assert_success(&config.delete("missing"));
}
#[test]
fn delete_session_distinguishes_case_sensitive_siblings() {
let config = SessionConfig::new();
let upper = config.create_session("Foo");
if config.session_dir("foo").exists() {
return; // This filesystem cannot store both spellings independently.
}
let lower = config.create_session("foo");
assert_success(&config.delete("foo"));
assert!(!lower.exists());
assert_eq!(
fs::read(upper.join("session.json")).unwrap(),
b"saved session"
);
}
#[cfg(unix)]
#[test]
fn delete_session_preserves_running_session() {
let config = SessionConfig::new();
let dir = config.create_session("Foo");
let _listener = std::os::unix::net::UnixListener::bind(dir.join("herdr.sock")).unwrap();
let output = config.delete("Foo");
assert_eq!(output.status.code(), Some(1));
let error: serde_json::Value = serde_json::from_slice(&output.stderr).unwrap();
assert!(error["error"]["message"]
.as_str()
.unwrap()
.contains("running"));
assert_eq!(
fs::read(dir.join("session.json")).unwrap(),
b"saved session"
);
}
#[test]
fn session_config_cleans_up_after_panic() {
let mut root = None;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let config = SessionConfig::new();
root = Some(config.root.clone());
config.create_session("Foo");
panic!("simulate a failed assertion");
}));
assert!(result.is_err());
assert!(!root.unwrap().exists());
}