mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-21 16:01:04 +00:00
* 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>
181 lines
5.3 KiB
Rust
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());
|
|
}
|