use super::cron::*;
use crate::core::types::{MachineTarget, Resource, ResourceType};
use std::path::{Path, PathBuf};
const CMD: &str = "/usr/local/bin/backup.sh";
fn cron(name: &str, schedule: &str, command: &str) -> Resource {
Resource {
resource_type: ResourceType::Cron,
machine: MachineTarget::Single("m1".to_string()),
owner: Some("root".to_string()),
name: Some(name.to_string()),
schedule: Some(schedule.to_string()),
command: Some(command.to_string()),
..Default::default()
}
}
fn absent(name: &str) -> Resource {
let mut r = cron(name, "0 3 * * *", CMD);
r.state = Some("absent".to_string());
r
}
fn write_exe(path: &Path, body: &str) {
std::fs::write(path, body).expect("write fake binary");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).expect("chmod");
}
}
struct FakeCrontab {
dir: PathBuf,
}
impl FakeCrontab {
fn new(tag: &str) -> Self {
let dir =
std::env::temp_dir().join(format!("forjar-cron-exec-{}-{}", tag, std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("bin")).expect("create fake bin dir");
let fx = FakeCrontab { dir };
write_exe(&fx.bin().join("crontab"), FAKE_CRONTAB);
write_exe(&fx.bin().join("sudo"), FAKE_SUDO);
write_exe(&fx.bin().join("id"), FAKE_ID);
fx
}
fn bin(&self) -> PathBuf {
self.dir.join("bin")
}
fn store(&self) -> PathBuf {
self.dir.join("crontab.store")
}
fn seed(&self, text: &str) {
std::fs::write(self.store(), text).expect("seed crontab store");
}
fn read(&self) -> String {
std::fs::read_to_string(self.store()).unwrap_or_default()
}
fn run(&self, script: &str) -> std::process::Output {
let path = format!(
"{}:{}",
self.bin().display(),
std::env::var("PATH").unwrap_or_default()
);
std::process::Command::new("bash")
.arg("-c")
.arg(script)
.env("PATH", path)
.env("FORJAR_TEST_CRON_STORE", self.store())
.output()
.expect("bash is available")
}
fn apply(&self, r: &Resource) {
let out = self.run(&apply_script(r));
assert!(
out.status.success(),
"apply script failed: {}",
String::from_utf8_lossy(&out.stderr)
);
}
fn observation(&self, r: &Resource) -> String {
let out = self.run(&state_query_script(r));
String::from_utf8_lossy(&out.stdout).to_string()
}
}
impl Drop for FakeCrontab {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
const FAKE_CRONTAB: &str = r#"#!/usr/bin/env bash
store="$FORJAR_TEST_CRON_STORE"
mode=""
while [ "$#" -gt 0 ]; do
case "$1" in
-u) shift 2 ;;
-l) mode=list; shift ;;
-) mode=write; shift ;;
*) shift ;;
esac
done
case "$mode" in
list) [ -f "$store" ] || exit 1; cat "$store" ;;
write) cat > "$store" ;;
esac
"#;
const FAKE_SUDO: &str = r#"#!/usr/bin/env bash
[ "$1" = "-n" ] && shift
exec "$@"
"#;
const FAKE_ID: &str = r#"#!/usr/bin/env bash
echo 1000
"#;
#[test]
fn fj362_the_observation_changes_when_the_schedule_is_tampered() {
let fx = FakeCrontab::new("t1");
let r = cron("backup", "0 3 * * *", CMD);
fx.apply(&r);
let before = fx.observation(&r);
fx.seed(&fx.read().replace("0 3 * * *", "0 4 * * *"));
let after = fx.observation(&r);
assert!(
before.contains("0 3 * * *"),
"the observation must contain the job it claims to watch: {before:?}"
);
assert_ne!(
before, after,
"a schedule tamper left the observation byte-identical: {before:?}"
);
}
#[test]
fn fj362_the_observation_changes_when_the_command_is_tampered() {
let fx = FakeCrontab::new("t1b");
let r = cron("backup", "0 3 * * *", CMD);
fx.apply(&r);
let before = fx.observation(&r);
fx.seed(&fx.read().replace(CMD, "/usr/bin/curl-evil"));
assert_ne!(
before,
fx.observation(&r),
"a command swap left the observation byte-identical: {before:?}"
);
}
#[test]
fn fj362_the_observation_still_changes_when_the_marker_is_deleted() {
let fx = FakeCrontab::new("t1c");
let r = cron("backup", "0 3 * * *", CMD);
fx.apply(&r);
let before = fx.observation(&r);
fx.seed(&fx.read().replace("# forjar:backup\n", ""));
assert_ne!(before, fx.observation(&r));
}
#[test]
fn fj362_a_config_schedule_edit_unschedules_the_old_job() {
let fx = FakeCrontab::new("t2");
fx.apply(&cron("backup", "0 3 * * *", CMD));
fx.apply(&cron("backup", "0 4 * * *", CMD));
let store = fx.read();
assert_eq!(
store.matches(CMD).count(),
1,
"the old schedule is still installed: {store:?}"
);
assert!(!store.contains("0 3 * * *"), "{store:?}");
}
#[test]
fn fj362_repairing_a_tampered_job_removes_the_tampered_line() {
let fx = FakeCrontab::new("t3");
let r = cron("backup", "0 3 * * *", CMD);
fx.apply(&r);
fx.seed(&fx.read().replace("0 3 * * *", "0 4 * * *"));
fx.apply(&r);
let store = fx.read();
assert_eq!(store.matches(CMD).count(), 1, "{store:?}");
assert!(!store.contains("0 4 * * *"), "{store:?}");
}
#[test]
fn fj362_three_applies_install_one_job() {
let fx = FakeCrontab::new("t4");
let r = cron("backup", "0 3 * * *", CMD);
fx.apply(&r);
fx.apply(&r);
fx.apply(&r);
let store = fx.read();
assert_eq!(store.matches(CMD).count(), 1, "{store:?}");
}
#[test]
fn fj362_absent_removes_the_job_and_spares_everything_else() {
let fx = FakeCrontab::new("t5");
fx.seed(concat!(
"MAILTO=me\n",
"# forjar:backup-db\n",
"# forjar-cmd:backup-db\n",
"0 5 * * * /usr/local/bin/dump-db.sh\n",
"# forjar:backup\n",
"# forjar-cmd:backup\n",
"0 3 * * * /usr/local/bin/backup.sh\n",
));
fx.apply(&absent("backup"));
let store = fx.read();
assert!(!store.contains(CMD), "the job survived absent: {store:?}");
assert!(store.contains("# forjar:backup-db"), "{store:?}");
assert!(store.contains("/usr/local/bin/dump-db.sh"), "{store:?}");
assert!(store.contains("MAILTO=me"), "{store:?}");
}
#[test]
fn fj362_every_generated_cron_script_survives_forjars_own_bashrs_gate() {
let present = cron("backup", "0 3 * * *", CMD);
let scripts = [
check_script(&present),
apply_script(&present),
apply_script(&absent("backup")),
state_query_script(&present),
];
for script in scripts {
crate::core::purifier::validate_script(&script)
.unwrap_or_else(|e| panic!("bashrs refused a generated cron script: {e}\n{script}"));
}
}
#[test]
fn fj362_check_does_not_mistake_a_prefix_sibling_for_the_job() {
let fx = FakeCrontab::new("t6");
fx.seed("# forjar:backup-db\n# forjar-cmd:backup-db\n0 5 * * * /usr/local/bin/dump-db.sh\n");
let out = fx.run(&check_script(&cron("backup", "0 3 * * *", CMD)));
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
assert!(
stdout.contains("missing:backup"),
"check claimed a prefix-sibling was the job: {stdout:?}"
);
assert!(!out.status.success(), "exit code disagreed with the marker");
}
#[test]
fn fj362_applying_a_job_spares_a_prefix_siblings_markers() {
let fx = FakeCrontab::new("t7");
fx.seed("# forjar:backup-db\n# forjar-cmd:backup-db\n0 5 * * * /usr/local/bin/dump-db.sh\n");
fx.apply(&cron("backup", "0 3 * * *", CMD));
let store = fx.read();
assert!(store.contains("# forjar:backup-db"), "{store:?}");
assert!(store.contains("# forjar-cmd:backup-db"), "{store:?}");
assert!(store.contains("/usr/local/bin/dump-db.sh"), "{store:?}");
}