#![allow(clippy::missing_docs_in_private_items)]
use super::*;
use crate::cron_jobs::CronJob;
fn test_job(id: &str) -> CronJob {
CronJob {
id: id.to_string(),
schedule: "@daily".to_string(),
handler: "test-handler".to_string(),
metadata: serde_json::json!({"key": "val"}),
machines: vec![crate::machine::current_machine()],
enabled: true,
source: "managed".to_string(),
created_at: 1000,
updated_at: 2000,
last_manual_trigger_at: Some(3000),
}
}
#[test]
fn metadata_roundtrip() {
let val = serde_json::json!({"key": "value", "num": 42});
let table = json_to_toml_table(&val);
let back = metadata_to_json(&table);
assert_eq!(back["key"], "value");
assert_eq!(back["num"], 42);
}
#[test]
fn metadata_roundtrip_empty_object() {
let val = serde_json::json!({});
let table = json_to_toml_table(&val);
let back = metadata_to_json(&table);
assert!(back.as_object().unwrap().is_empty());
}
#[test]
fn json_to_toml_table_non_object_returns_empty() {
let val = serde_json::json!([1, 2, 3]);
let table = json_to_toml_table(&val);
assert!(table.is_empty());
}
#[test]
fn json_to_toml_table_null_returns_empty() {
let table = json_to_toml_table(&serde_json::Value::Null);
assert!(table.is_empty());
}
#[test]
fn read_job_toml_missing_returns_none() {
let path = std::path::PathBuf::from("/nonexistent/path/job.toml");
assert!(read_job_toml(&path).is_none());
}
#[test]
fn write_and_load_roundtrip() {
let id = "test-write-load-roundtrip";
let job = test_job(id);
write_job(&job).expect("write_job failed");
let loaded = load_job_from_dir(id).expect("load_job_from_dir failed");
assert_eq!(loaded.id, job.id);
assert_eq!(loaded.schedule, job.schedule);
assert_eq!(loaded.handler, job.handler);
assert_eq!(loaded.enabled, job.enabled);
assert_eq!(loaded.created_at, job.created_at);
assert_eq!(loaded.updated_at, job.updated_at);
assert_eq!(loaded.last_manual_trigger_at, job.last_manual_trigger_at);
assert_eq!(loaded.metadata["key"], "val");
remove_job_dir(id).expect("cleanup failed");
}
#[test]
fn remove_job_dir_nonexistent_is_ok() {
assert!(remove_job_dir("test-nonexistent-9999999").is_ok());
}
#[test]
fn remove_job_dir_removes_directory() {
let id = "test-remove-dir";
let job = test_job(id);
write_job(&job).expect("write_job failed");
let dir = crate::paths::job_dir(id);
assert!(dir.exists());
remove_job_dir(id).expect("remove failed");
assert!(!dir.exists());
}
#[test]
fn load_store_returns_written_job() {
let id = "test-load-store-job";
let job = test_job(id);
write_job(&job).expect("write_job failed");
let store = load_store();
let loaded = store.lock().unwrap().get(id).cloned();
assert!(loaded.is_some(), "job not found in loaded store");
assert_eq!(loaded.unwrap().handler, "test-handler");
remove_job_dir(id).expect("cleanup failed");
}
#[test]
fn write_job_creates_gitignore() {
let id = "test-gitignore-creation";
let job = test_job(id);
write_job(&job).expect("write_job failed");
let gitignore = crate::paths::job_gitignore_path(id);
assert!(gitignore.exists());
let content = std::fs::read_to_string(&gitignore).unwrap();
assert!(content.contains("*.local.*"));
assert!(content.contains("run.sh"));
remove_job_dir(id).expect("cleanup failed");
}
#[test]
fn local_toml_overrides_base_handler() {
let id = "test-local-override-handler";
let job = test_job(id);
write_job(&job).expect("write_job failed");
let local_path = crate::paths::job_local_toml_path(id);
std::fs::write(
&local_path,
"handler = \"overridden\"\n\n[metadata]\nlocal_key = \"local_value\"\n",
)
.unwrap();
let loaded = load_job_from_dir(id).expect("load failed");
assert_eq!(loaded.handler, "overridden");
assert_eq!(loaded.metadata["local_key"], "local_value");
remove_job_dir(id).expect("cleanup failed");
}
#[test]
fn write_job_twice_does_not_fail_on_existing_gitignore() {
let id = "test-write-idempotent";
let job = test_job(id);
write_job(&job).expect("first write");
write_job(&job).expect("second write (gitignore already exists)");
remove_job_dir(id).expect("cleanup failed");
}
#[test]
fn load_store_skips_non_directory_entries() {
let jobs_dir = crate::paths::jobs_dir();
std::fs::create_dir_all(&jobs_dir).unwrap();
let fake = jobs_dir.join("not-a-job.txt");
std::fs::write(&fake, "hello").unwrap();
let store = load_store();
let _ = store;
std::fs::remove_file(&fake).unwrap();
}
#[test]
fn load_store_from_dir_missing_dir_returns_empty_store() {
let store = load_store_from_dir(std::path::Path::new("/nonexistent-jobs-dir-99999"));
assert!(store.lock().unwrap().is_empty());
}
#[test]
fn write_job_errors_when_job_dir_path_is_a_file() {
let id = "test-write-blocked-by-file";
let jobs_dir = crate::paths::jobs_dir();
std::fs::create_dir_all(&jobs_dir).unwrap();
let blocker = jobs_dir.join(id);
let _ = std::fs::remove_dir_all(&blocker);
std::fs::write(&blocker, "i block the job dir").unwrap();
let err = write_job(&test_job(id)).unwrap_err();
let _ = err;
assert!(blocker.is_file(), "the blocking file is left untouched");
std::fs::remove_file(&blocker).unwrap();
}
#[test]
fn write_job_errors_when_job_toml_path_is_a_directory() {
let id = "test-write-toml-blocked-by-dir";
let dir = crate::paths::job_dir(id);
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(crate::paths::job_toml_path(id)).unwrap();
let err = write_job(&test_job(id)).unwrap_err();
let _ = err;
assert!(crate::paths::job_toml_path(id).is_dir());
remove_job_dir(id).expect("cleanup failed");
}
#[test]
fn json_to_toml_table_skips_non_representable_values() {
let val = serde_json::json!({"keep": "yes", "drop": serde_json::Value::Null});
let table = json_to_toml_table(&val);
assert!(table.contains_key("keep"));
assert!(
!table.contains_key("drop"),
"a null value is not representable in TOML and must be skipped"
);
}
#[test]
fn load_job_from_dir_torn_toml_returns_none() {
let id = "test-torn-job-toml";
let dir = crate::paths::job_dir(id);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(crate::paths::job_toml_path(id), "schedule = \"@dai").unwrap();
assert!(load_job_from_dir(id).is_none());
remove_job_dir(id).expect("cleanup failed");
}
#[test]
fn load_job_from_dir_missing_schedule_returns_none() {
let id = "test-job-missing-schedule";
let dir = crate::paths::job_dir(id);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
crate::paths::job_toml_path(id),
"handler = \"h\"\nenabled = true\n",
)
.unwrap();
assert!(load_job_from_dir(id).is_none());
remove_job_dir(id).expect("cleanup failed");
}
struct HomeOverrideGuard {
dir: std::path::PathBuf,
previous: Option<std::ffi::OsString>,
}
impl HomeOverrideGuard {
fn new() -> Self {
let dir = std::env::temp_dir().join(format!("moadim-st-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).expect("create temp home");
let previous = std::env::var_os("MOADIM_HOME_OVERRIDE");
unsafe { std::env::set_var("MOADIM_HOME_OVERRIDE", &dir) }
Self { dir, previous }
}
}
impl Drop for HomeOverrideGuard {
fn drop(&mut self) {
unsafe {
match self.previous.take() {
Some(val) => std::env::set_var("MOADIM_HOME_OVERRIDE", val),
None => std::env::remove_var("MOADIM_HOME_OVERRIDE"),
}
}
restore_writable_storage(&self.dir);
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn restore_writable_storage(dir: &std::path::Path) {
use std::os::unix::fs::PermissionsExt as _;
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755));
if path.is_dir() {
restore_writable_storage(&path);
}
}
}
let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o755));
}
#[test]
fn load_job_from_dir_missing_handler_returns_none() {
let _home = HomeOverrideGuard::new();
let id = "missing-handler-job";
let dir = crate::paths::job_dir(id);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
crate::paths::job_toml_path(id),
"schedule = \"@daily\"\nenabled = true\n",
)
.unwrap();
assert!(load_job_from_dir(id).is_none());
}
#[cfg(unix)]
#[test]
fn write_job_gitignore_write_failure_returns_err() {
use std::os::unix::fs::PermissionsExt as _;
let home = HomeOverrideGuard::new();
let job = test_job("gitignore-fail");
let dir = home
.dir
.join(".config")
.join("moadim")
.join("jobs")
.join(&job.id);
std::fs::create_dir_all(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o555)).unwrap();
let result = write_job(&job);
assert!(result.is_err());
}
#[cfg(unix)]
#[test]
fn remove_job_dir_remove_failure_returns_err() {
use std::os::unix::fs::PermissionsExt as _;
let home = HomeOverrideGuard::new();
let jobs = home.dir.join(".config").join("moadim").join("jobs");
let job_dir = jobs.join("rd-fail");
std::fs::create_dir_all(&job_dir).unwrap();
std::fs::set_permissions(&jobs, std::fs::Permissions::from_mode(0o555)).unwrap();
let result = remove_job_dir("rd-fail");
assert!(result.is_err());
}